From b886cc22fe29498fe55801b86f45d941a4ba40cf Mon Sep 17 00:00:00 2001 From: Yuhan Zou Date: Thu, 25 Jan 2018 08:23:20 +0800 Subject: [PATCH 01/26] Release 1.7.4 1. ConcurrentLoggingConsumer supported Win32 platforms. --- sensorsanalytics/sdk.py | 109 +++++++++++++++++++++++++++++++++++----- setup.py | 2 +- 2 files changed, 98 insertions(+), 13 deletions(-) diff --git a/sensorsanalytics/sdk.py b/sensorsanalytics/sdk.py index b3921a9..04ac568 100644 --- a/sensorsanalytics/sdk.py +++ b/sensorsanalytics/sdk.py @@ -8,6 +8,7 @@ import json import logging import logging.handlers +import os import re import sys import threading @@ -25,7 +26,7 @@ import urllib2 import urllib -SDK_VERSION = '1.7.3' +SDK_VERSION = '1.7.4' try: isinstance("", basestring) @@ -61,6 +62,13 @@ class SensorsAnalyticsNetworkException(SensorsAnalyticsException): pass +class SensorsAnalyticsFileLockException(SensorsAnalyticsException): + """ + 当 ConcurrentLoggingConsumer 文件锁异常时,SDK 会抛出此异常,用户应当捕获并记录错误日志。 + """ + pass + + class SensorsAnalyticsDebugException(Exception): """ Debug模式专用的异常 @@ -68,6 +76,86 @@ class SensorsAnalyticsDebugException(Exception): pass +if os.name == 'nt': # pragma: no cover + import msvcrt + + LOCK_EX = 0x1 + LOCK_SH = 0x2 + LOCK_NB = 0x4 + LOCK_UN = msvcrt.LK_UNLCK + + def lock(file_, flags): + if flags & LOCK_SH: + if flags & LOCK_NB: + mode = msvcrt.LK_NBRLCK + else: + if hasattr(msvcrt, 'LK.RLOCK'): + mode = msvcrt.LK_RLOCK + else: + mode = msvcrt.LK_RLCK + else: + if flags & LOCK_NB: + mode = msvcrt.LK_NBLCK + else: + mode = msvcrt.LK_LOCK + + try: + savepos = file_.tell() + + file_.seek(0, os.SEEK_END) + tellpos = file_.tell() + + file_.seek(0) + try: + msvcrt.locking(file_.fileno(), mode, tellpos) + except IOError as e: + raise SensorsAnalyticsFileLockException(e) + finally: + if savepos: + file_.seek(savepos) + except IOError as e: + raise SensorsAnalyticsFileLockException(e) + + def unlock(file_): + try: + savepos = file_.tell() + + file_.seek(0, os.SEEK_END) + tellpos = file_.tell() + + file_.seek(0) + + try: + msvcrt.locking(file_.fileno(), LOCK_UN, tellpos) + except IOError as e: + raise SensorsAnalyticsFileLockException(e) + finally: + if savepos: + file_.seek(savepos) + except IOError as e: + raise SensorsAnalyticsFileLockException(e) + +elif os.name == 'posix': # pragma: no cover + import fcntl + + LOCK_EX = fcntl.LOCK_EX + LOCK_SH = fcntl.LOCK_SH + LOCK_NB = fcntl.LOCK_NB + LOCK_UN = fcntl.LOCK_UN + + def lock(file_, flags): + try: + fcntl.flock(file_.fileno(), flags) + except IOError as e: + raise SensorsAnalyticsFileLockException(e) + + def unlock(file_): + fcntl.flock(file_.fileno(), LOCK_UN) + +else: + raise SensorsAnalyticsFileLockException("SensorsAnalytics SDK is defined for NT and POSIX system.") + + class SensorsAnalytics(object): """ 使用一个 SensorsAnalytics 的实例来进行数据发送。 @@ -744,17 +832,14 @@ def isValid(self, filename): return self._filename == filename def write(self, messages): - try: - fcntl.flock(self._file, fcntl.LOCK_EX) - - for message in messages: - self._file.write(message) - self._file.write('\n') - self._file.flush() - - fcntl.flock(self._file, fcntl.LOCK_UN) - except IOError: - print("can't lock the file") + lock(self._file, LOCK_EX) + + for message in messages: + self._file.write(message) + self._file.write('\n') + self._file.flush() + + unlock(self._file) @classmethod diff --git a/setup.py b/setup.py index 57e7736..118bc0d 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='SensorsAnalyticsSDK', - version='1.7.3', + version='1.7.4', author='Yuhan ZOU', author_email='zouyuhan@sensorsdata.cn', url='http://www.sensorsdata.cn', From 2af28ed089674027c834b7d21533c85b5f0b6ed9 Mon Sep 17 00:00:00 2001 From: Yuhan Zou Date: Thu, 1 Feb 2018 15:05:04 +0800 Subject: [PATCH 02/26] Bug fixed for ConcurrentLoggingConsumer --- sensorsanalytics/sdk.py | 54 +++++++++-------------------------------- 1 file changed, 12 insertions(+), 42 deletions(-) diff --git a/sensorsanalytics/sdk.py b/sensorsanalytics/sdk.py index 04ac568..a551495 100644 --- a/sensorsanalytics/sdk.py +++ b/sensorsanalytics/sdk.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import base64 import datetime -import fcntl import gzip import json import logging @@ -79,35 +78,14 @@ class SensorsAnalyticsDebugException(Exception): if os.name == 'nt': # pragma: no cover import msvcrt - LOCK_EX = 0x1 - LOCK_SH = 0x2 - LOCK_NB = 0x4 - LOCK_UN = msvcrt.LK_UNLCK - - def lock(file_, flags): - if flags & LOCK_SH: - if flags & LOCK_NB: - mode = msvcrt.LK_NBRLCK - else: - if hasattr(msvcrt, 'LK.RLOCK'): - mode = msvcrt.LK_RLOCK - else: - mode = msvcrt.LK_RLCK - else: - if flags & LOCK_NB: - mode = msvcrt.LK_NBLCK - else: - mode = msvcrt.LK_LOCK - + def lock(file_): try: savepos = file_.tell() - - file_.seek(0, os.SEEK_END) - tellpos = file_.tell() - + file_.seek(0) + try: - msvcrt.locking(file_.fileno(), mode, tellpos) + msvcrt.locking(file_.fileno(), msvcrt.LK_LOCK, 1) except IOError as e: raise SensorsAnalyticsFileLockException(e) finally: @@ -119,14 +97,11 @@ def lock(file_, flags): def unlock(file_): try: savepos = file_.tell() - - file_.seek(0, os.SEEK_END) - tellpos = file_.tell() - - file_.seek(0) + if savepos: + file_.seek(0) try: - msvcrt.locking(file_.fileno(), LOCK_UN, tellpos) + msvcrt.locking(file_.fileno(), msvcrt.LK_UNLCK, 1) except IOError as e: raise SensorsAnalyticsFileLockException(e) finally: @@ -138,19 +113,14 @@ def unlock(file_): elif os.name == 'posix': # pragma: no cover import fcntl - LOCK_EX = fcntl.LOCK_EX - LOCK_SH = fcntl.LOCK_SH - LOCK_NB = fcntl.LOCK_NB - LOCK_UN = fcntl.LOCK_UN - - def lock(file_, flags): + def lock(file_): try: - fcntl.flock(file_.fileno(), flags) + fcntl.flock(file_.fileno(), fcntl.LOCK_EX) except IOError as e: raise SensorsAnalyticsFileLockException(e) def unlock(file_): - fcntl.flock(file_.fileno(), LOCK_UN) + fcntl.flock(file_.fileno(), fcntl.LOCK_UN) else: raise SensorsAnalyticsFileLockException("SensorsAnalytics SDK is defined for NT and POSIX system.") @@ -662,7 +632,7 @@ def flush(self): def sync_flush(self, throw_exception=True): """ - 执行一次同步发送。 throw_exception 表示在发送失败时是否向外抛出异常。 + 执行一次同步发送。 throw_exception 表示在发送失败时是否向外抛出异常 """ flush_success = False @@ -832,7 +802,7 @@ def isValid(self, filename): return self._filename == filename def write(self, messages): - lock(self._file, LOCK_EX) + lock(self._file) for message in messages: self._file.write(message) From cb50db51c8e6884a585c9032f435b740666972bb Mon Sep 17 00:00:00 2001 From: Yuhan Zou Date: Fri, 23 Feb 2018 01:25:56 +0800 Subject: [PATCH 03/26] Release 1.7.5 1. Bug fixed for ConcurrentLoggingConsumer --- sensorsanalytics/sdk.py | 28 ++++++++++++++++++---------- setup.py | 2 +- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/sensorsanalytics/sdk.py b/sensorsanalytics/sdk.py index a551495..71d9310 100644 --- a/sensorsanalytics/sdk.py +++ b/sensorsanalytics/sdk.py @@ -25,7 +25,7 @@ import urllib2 import urllib -SDK_VERSION = '1.7.4' +SDK_VERSION = '1.7.5' try: isinstance("", basestring) @@ -126,6 +126,18 @@ def unlock(file_): raise SensorsAnalyticsFileLockException("SensorsAnalytics SDK is defined for NT and POSIX system.") +class SAFileLock(object): + + def __init__(self, file_handler): + self._file_handler = file_handler + + def __enter__(self): + lock(self._file_handler) + return self + + def __exit__(self, t, v, tb): + unlock(self._file_handler) + class SensorsAnalytics(object): """ 使用一个 SensorsAnalytics 的实例来进行数据发送。 @@ -802,15 +814,11 @@ def isValid(self, filename): return self._filename == filename def write(self, messages): - lock(self._file) - - for message in messages: - self._file.write(message) - self._file.write('\n') - self._file.flush() - - unlock(self._file) - + with SAFileLock(self._file): + for message in messages: + self._file.write(message) + self._file.write('\n') + self._file.flush() @classmethod def construct_filename(cls, prefix): diff --git a/setup.py b/setup.py index 118bc0d..436a138 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='SensorsAnalyticsSDK', - version='1.7.4', + version='1.7.5', author='Yuhan ZOU', author_email='zouyuhan@sensorsdata.cn', url='http://www.sensorsdata.cn', From f4a1dd779db54ac77c6a9d91241e1383111e2f70 Mon Sep 17 00:00:00 2001 From: guanwei Date: Thu, 18 Oct 2018 14:22:29 +0800 Subject: [PATCH 04/26] add item_set item_delete --- sensorsanalytics/sdk.py | 110 +++++++++++++++++++++++++++++++--------- 1 file changed, 86 insertions(+), 24 deletions(-) diff --git a/sensorsanalytics/sdk.py b/sensorsanalytics/sdk.py index 71d9310..fa63d8b 100644 --- a/sensorsanalytics/sdk.py +++ b/sensorsanalytics/sdk.py @@ -240,6 +240,32 @@ def track_signup(self, distinct_id, original_id, properties=None): self._track_event('track_signup', '$SignUp', distinct_id, original_id, all_properties, False) + @staticmethod + def _normalize_properties(data): + if "properties" in data and data["properties"] is not None: + for key, value in data["properties"].items(): + if not is_str(key): + raise SensorsAnalyticsIllegalDataException("property key must be a str. [key=%s]" % str(key)) + if len(key) > 255: + raise SensorsAnalyticsIllegalDataException("the max length of property key is 256. [key=%s]" % str(key)) + if not SensorsAnalytics.NAME_PATTERN.match(key): + raise SensorsAnalyticsIllegalDataException( + "property key must be a valid variable name. [key=%s]" % str(key)) + + if is_str(value) and len(value) > 8191: + raise SensorsAnalyticsIllegalDataException("the max length of property key is 8192. [key=%s]" % str(key)) + + if not is_str(value) and not is_int(value) and not isinstance(value, float)\ + and not isinstance(value, datetime.datetime) and not isinstance(value, datetime.date)\ + and not isinstance(value, list) and value is not None: + raise SensorsAnalyticsIllegalDataException( + "property value must be a str/int/float/datetime/date/list. [value=%s]" % type(value)) + if isinstance(value, list): + for lvalue in value: + if not is_str(lvalue): + raise SensorsAnalyticsIllegalDataException( + "[list] property's value must be a str. [value=%s]" % type(lvalue)) + @staticmethod def _normalize_data(data): # 检查 distinct_id @@ -271,30 +297,7 @@ def _normalize_data(data): raise SensorsAnalyticsIllegalDataException("project name must be a valid variable name. [project=%s]" % data['project']) # 检查 properties - if "properties" in data and data["properties"] is not None: - for key, value in data["properties"].items(): - if not is_str(key): - raise SensorsAnalyticsIllegalDataException("property key must be a str. [key=%s]" % str(key)) - if len(key) > 255: - raise SensorsAnalyticsIllegalDataException("the max length of property key is 256. [key=%s]" % str(key)) - if not SensorsAnalytics.NAME_PATTERN.match(key): - raise SensorsAnalyticsIllegalDataException( - "property key must be a valid variable name. [key=%s]" % str(key)) - - if is_str(value) and len(value) > 8191: - raise SensorsAnalyticsIllegalDataException("the max length of property key is 8192. [key=%s]" % str(key)) - - if not is_str(value) and not is_int(value) and not isinstance(value, float)\ - and not isinstance(value, datetime.datetime) and not isinstance(value, datetime.date)\ - and not isinstance(value, list) and value is not None: - raise SensorsAnalyticsIllegalDataException( - "property value must be a str/int/float/datetime/date/list. [value=%s]" % type(value)) - if isinstance(value, list): - for lvalue in value: - if not is_str(lvalue): - raise SensorsAnalyticsIllegalDataException( - "[list] property's value must be a str. [value=%s]" % type(lvalue)) - + SensorsAnalytics._normalize_properties(data) return data def _get_lib_properties(self): @@ -416,6 +419,65 @@ def profile_delete(self, distinct_id, is_login_id=False): """ return self._track_event('profile_delete', None, distinct_id, None, {}, is_login_id) + def item_set(self, item_type, item_id, properties={}): + """ + 直接设置一个物品,如果已存在则覆盖。 + + :param item_type: 物品类型 + :param item_id: 物品的唯一标识 + :param profiles: 物品属性 + """ + return self._track_item('item_set', item_type, item_id, properties) + + def item_delete(self, item_type, item_id, properties={}): + """ + 删除一个物品。 + + :param item_type: 物品类型 + :param item_id: 物品的唯一标识 + :param profiles: 物品属性 + """ + return self._track_item('item_delete', item_type, item_id, properties) + + @staticmethod + def _normalize_item_data(data): + # 检查 item_type + if not SensorsAnalytics.NAME_PATTERN.match(data['item_type']): + raise SensorsAnalyticsIllegalDataException( + "item_type must be a valid variable name. [key=%s]" % str(data['item_type'])) + + # 检查 item_id + if data['item_id'] is None or len(str(data['item_id'])) == 0: + raise SensorsAnalyticsIllegalDataException("item_id must not be empty") + if len(str(data['item_id'])) > 255: + raise SensorsAnalyticsIllegalDataException("the max length of item_id is 255") + # 检查 properties + SensorsAnalytics._normalize_properties(data) + return data + + def _track_item(self, event_type, item_type, item_id, properties): + data = { + 'type': event_type, + 'time': self._now(), + 'lib': self._get_lib_properties(), + 'item_type': item_type, + 'item_id': item_id, + } + + if self._default_project_name is not None: + data['project'] = self._default_project_name + + if properties and '$project' in properties and len(str(properties['$project'])) != 0: + data['project'] = properties['$project'] + properties.pop('$project') + + data['properties'] = properties + + data = self._normalize_item_data(data) + self._json_dumps(data) + self._consumer.send(self._json_dumps(data)) + + def _track_event(self, event_type, event_name, distinct_id, original_id, properties, is_login_id): event_time = self._extract_user_time(properties) or self._now() From cd985913c57ea33a2afcbff0a7f10587429be856 Mon Sep 17 00:00:00 2001 From: guanwei Date: Thu, 18 Oct 2018 14:41:33 +0800 Subject: [PATCH 05/26] change {} to None --- sensorsanalytics/sdk.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sensorsanalytics/sdk.py b/sensorsanalytics/sdk.py index fa63d8b..07c2859 100644 --- a/sensorsanalytics/sdk.py +++ b/sensorsanalytics/sdk.py @@ -419,7 +419,7 @@ def profile_delete(self, distinct_id, is_login_id=False): """ return self._track_event('profile_delete', None, distinct_id, None, {}, is_login_id) - def item_set(self, item_type, item_id, properties={}): + def item_set(self, item_type, item_id, properties=None): """ 直接设置一个物品,如果已存在则覆盖。 @@ -429,7 +429,7 @@ def item_set(self, item_type, item_id, properties={}): """ return self._track_item('item_set', item_type, item_id, properties) - def item_delete(self, item_type, item_id, properties={}): + def item_delete(self, item_type, item_id, properties=None): """ 删除一个物品。 @@ -456,6 +456,8 @@ def _normalize_item_data(data): return data def _track_item(self, event_type, item_type, item_id, properties): + if properties is None: + properties = {} data = { 'type': event_type, 'time': self._now(), From 5dbe39fc200f8195c6ceab79396d86de0b23d004 Mon Sep 17 00:00:00 2001 From: guanwei Date: Tue, 13 Nov 2018 17:10:57 +0800 Subject: [PATCH 06/26] modify version --- sensorsanalytics/sdk.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sensorsanalytics/sdk.py b/sensorsanalytics/sdk.py index 07c2859..89f727c 100644 --- a/sensorsanalytics/sdk.py +++ b/sensorsanalytics/sdk.py @@ -25,7 +25,7 @@ import urllib2 import urllib -SDK_VERSION = '1.7.5' +SDK_VERSION = '1.10.0' try: isinstance("", basestring) diff --git a/setup.py b/setup.py index 436a138..8d3bcc4 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='SensorsAnalyticsSDK', - version='1.7.5', + version='1.10.0', author='Yuhan ZOU', author_email='zouyuhan@sensorsdata.cn', url='http://www.sensorsdata.cn', From c542de1a46f76418f2236ee2c0ced3e2d37141a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B2=B3=E5=89=91=E9=92=9F?= Date: Tue, 7 May 2019 15:47:20 +0800 Subject: [PATCH 07/26] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=EF=BC=8C=E6=B7=BB=E5=8A=A0=20license?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 202 ++++++++++++++++++++++++++++++++++++++++ README.md | 17 ++++ sensorsanalytics/sdk.py | 8 +- 3 files changed, 223 insertions(+), 4 deletions(-) create mode 100755 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..56a0959 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2015-2019 Sensors Data Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/README.md b/README.md index 0a2cfad..779e275 100644 --- a/README.md +++ b/README.md @@ -30,3 +30,20 @@ Once the SDK is successfully installed, use the Sensors Analytics SDK likes: See our [full manual](http://www.sensorsdata.cn/manual/python_sdk.html) +## License + +Copyright 2015-2019 Sensors Data Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +**禁止一切基于神策数据 Python 开源 SDK 的所有商业活动!** \ No newline at end of file diff --git a/sensorsanalytics/sdk.py b/sensorsanalytics/sdk.py index 89f727c..cfb5933 100644 --- a/sensorsanalytics/sdk.py +++ b/sensorsanalytics/sdk.py @@ -531,7 +531,7 @@ def __init__(self, url_prefix, request_timeout=None): 初始化 Consumer。 :param url_prefix: 服务器的 URL 地址。 - :param request_timeout: 请求的超时时间,单位毫秒。 + :param request_timeout: 请求的超时时间,单位为秒。 """ self._url_prefix = url_prefix self._request_timeout = request_timeout @@ -595,7 +595,7 @@ def __init__(self, url_prefix, max_size=50, request_timeout=None): :param url_prefix: 服务器的 URL 地址。 :param max_size: 批量发送的阈值。 - :param request_timeout: 请求服务器的超时时间,单位毫秒。 + :param request_timeout: 请求服务器的超时时间,单位为秒。 :return: """ super(BatchConsumer, self).__init__(url_prefix, request_timeout) @@ -674,7 +674,7 @@ def __init__(self, url_prefix, flush_max_time=3, flush_size=20, :param flush_size: 队列缓存的阈值,超过此值将立即进行发送。 :param max_batch_size: 单个请求发送的最大大小。 :param max_size: 整个缓存队列的最大大小。 - :param request_timeout: 请求的超时时间,单位毫秒。 + :param request_timeout: 请求的超时时间,单位为秒。 """ super(AsyncBatchConsumer, self).__init__(url_prefix, request_timeout) @@ -751,7 +751,7 @@ def __init__(self, url_prefix, write_data=True, request_timeout=None): 初始化Consumer :param url_prefix: 服务器提供的用于Debug的API的URL地址,特别注意,它与导入数据的API并不是同一个 :param write_data: 发送过去的数据,是真正写入,还是仅仅进行检查 - :param request_timeout:请求的超时时间,单位毫秒 + :param request_timeout:请求的超时时间,单位为秒 :return: """ debug_url = urlparse(url_prefix) From 0a4e5aeead1e116b3f33f7041a0c34027c5695e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B2=B3=E5=89=91=E9=92=9F?= Date: Tue, 18 Jun 2019 20:42:50 +0800 Subject: [PATCH 08/26] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20$project=20=E5=92=8C?= =?UTF-8?q?=20$token=20=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 9 +++++++++ README.md | 2 ++ sensorsanalytics/sdk.py | 32 +++++++++++++++++++++++++++++++- sensorsanalytics/test_sdk.py | 13 ++++++++----- setup.py | 2 +- 5 files changed, 51 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index e75beff..4d0f3f4 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,12 @@ profile # AppCode .idea + +# dist +/dist/ + +# MANIFEST +MANIFEST + +#local +local.properties diff --git a/README.md b/README.md index 779e275..0be2d76 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ Once the SDK is successfully installed, use the Sensors Analytics SDK likes: ## To learn more See our [full manual](http://www.sensorsdata.cn/manual/python_sdk.html) +或者加入神策官方 SDK QQ 讨论群:

+![ QQ 讨论群](https://github.com/sensorsdata/sa-sdk-android/raw/master/docs/qrCode.jpeg) ## License diff --git a/sensorsanalytics/sdk.py b/sensorsanalytics/sdk.py index cfb5933..e2a7beb 100644 --- a/sensorsanalytics/sdk.py +++ b/sensorsanalytics/sdk.py @@ -25,7 +25,7 @@ import urllib2 import urllib -SDK_VERSION = '1.10.0' +SDK_VERSION = '1.10.1' try: isinstance("", basestring) @@ -364,6 +364,28 @@ def _extract_user_time(properties): return t return None + @staticmethod + def _extract_token(properties): + """ + 如果用户传入了 $token 字段,则在 properties 外层加上token,并删除 $token 字段 + """ + if properties is not None and '$token' in properties: + t = properties['$token'] + del (properties['$token']) + return t + return None + + @staticmethod + def _extract_project(properties): + """ + 如果用户传入了 $project 字段,则在 properties 外层加上 project,并删除 $project 字段 + """ + if properties is not None and '$project' in properties: + t = properties['$project'] + del (properties['$project']) + return t + return None + def profile_set(self, distinct_id, profiles, is_login_id=False): """ 直接设置一个用户的 Profile,如果已存在则覆盖 @@ -482,6 +504,8 @@ def _track_item(self, event_type, item_type, item_id, properties): def _track_event(self, event_type, event_name, distinct_id, original_id, properties, is_login_id): event_time = self._extract_user_time(properties) or self._now() + event_token = self._extract_token(properties) + event_project = self._extract_project(properties) data = { 'type': event_type, @@ -505,6 +529,12 @@ def _track_event(self, event_type, event_name, distinct_id, original_id, propert if is_login_id: properties["$is_login_id"] = True + if event_token is not None: + data["token"] = event_token + + if event_project is not None: + data["project"] = event_project + data = self._normalize_data(data) self._consumer.send(self._json_dumps(data)) diff --git a/sensorsanalytics/test_sdk.py b/sensorsanalytics/test_sdk.py index 3783a04..ddd95e2 100644 --- a/sensorsanalytics/test_sdk.py +++ b/sensorsanalytics/test_sdk.py @@ -6,8 +6,8 @@ from sdk import * -TEST_URL_PREFIX = 'http://10.10.11.209:8006/sa?token=bbb' -TEST_DEBUG_URL_PREFIX = 'http://10.10.11.209:8006/debug?token=bbb' +TEST_URL_PREFIX = 'https://sdk-test.datasink.sensorsdata.cn/sa?project=yuejianzhong&token=95c73ae661f85aa0' +TEST_DEBUG_URL_PREFIX = 'https://sdk-test.datasink.sensorsdata.cn/sa?project=yuejianzhong&token=95c73ae661f85aa0' class NormalTest(unittest.TestCase): @@ -53,11 +53,14 @@ def clear_msg_counter(self): def testDebug(self): consumer = DebugConsumer(TEST_DEBUG_URL_PREFIX, False) sa = SensorsAnalytics(consumer) - sa.track(1234, 'Test', {'From': 'Baidu'}, is_login_id=True) + sa.track(1234, 'Test', {'From1': 'Baidu'}, is_login_id=True) consumer = DebugConsumer(TEST_DEBUG_URL_PREFIX, True) sa = SensorsAnalytics(consumer) - sa.track(1234, 'Test', {'From': 456}) - sa.track(1234, 'Test', {'From': 'Baidu'}) + sa.track(1234, 'Test', {'From2': 456}) + sa.track(1234, 'Test', {'From1': 'Baidu'}) + sa.track(1234, 'Test', {'From1': 'Baidu', '$project': "yuejianzhong"}) + sa.track(1234, 'Test', {'From1': 'Baidu', '$token': "dhuw393jdcioj39"}) + sa.track(1234, 'Test', {'From1': 'Baidu', '$token': "dhuw393jdcioj39",'$project': "yuejianzhong"}) def testNormal(self): consumer = DefaultConsumer(TEST_URL_PREFIX) diff --git a/setup.py b/setup.py index 8d3bcc4..08c9042 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='SensorsAnalyticsSDK', - version='1.10.0', + version='1.10.1', author='Yuhan ZOU', author_email='zouyuhan@sensorsdata.cn', url='http://www.sensorsdata.cn', From e96642494621220b2dc28ce5dbf5069fdfef52e0 Mon Sep 17 00:00:00 2001 From: YueJZSensorsData <36151500+YueJZSensorsData@users.noreply.github.com> Date: Fri, 27 Dec 2019 11:20:36 +0800 Subject: [PATCH 09/26] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0be2d76..8ed6184 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ See our [full manual](http://www.sensorsdata.cn/manual/python_sdk.html) ## License -Copyright 2015-2019 Sensors Data Inc. +Copyright 2015-2020 Sensors Data Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -48,4 +48,4 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -**禁止一切基于神策数据 Python 开源 SDK 的所有商业活动!** \ No newline at end of file +**禁止一切基于神策数据 Python 开源 SDK 的所有商业活动!** From de68b0e645ec1ce5cb2cdd2a2fe2fc46fb4893a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B2=B3=E5=89=91=E9=92=9F?= Date: Tue, 31 Dec 2019 14:42:55 +0800 Subject: [PATCH 10/26] =?UTF-8?q?=E4=BF=AE=E6=94=B9=20license?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 2 +- LICENSE.txt | 13 ------------- README.md | 2 +- README.txt | 14 -------------- setup.py | 27 +++++++++++++++------------ 5 files changed, 17 insertions(+), 41 deletions(-) delete mode 100644 LICENSE.txt delete mode 100644 README.txt diff --git a/LICENSE b/LICENSE index 56a0959..d541085 100755 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2015-2019 Sensors Data Inc. + Copyright 2015-2020 Sensors Data Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index 8b48d57..0000000 --- a/LICENSE.txt +++ /dev/null @@ -1,13 +0,0 @@ -Copyright 2015 SensorsData Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/README.md b/README.md index 8ed6184..bbc6437 100644 --- a/README.md +++ b/README.md @@ -48,4 +48,4 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -**禁止一切基于神策数据 Python 开源 SDK 的所有商业活动!** +**禁止一切基于神策数据 Python 开源 SDK 的所有商业活动!** \ No newline at end of file diff --git a/README.txt b/README.txt deleted file mode 100644 index 4fecb8c..0000000 --- a/README.txt +++ /dev/null @@ -1,14 +0,0 @@ -===================== -Sensors Analytics SDK -===================== - -Sensors Analytics SDK -===================== - -This is the official Python SDK for Sensors Analytics. - -To Learn More -------------- - -See our `full manual `_. - diff --git a/setup.py b/setup.py index 08c9042..4493917 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,16 @@ -from distutils.core import setup +import setuptools -setup( - name='SensorsAnalyticsSDK', - version='1.10.1', - author='Yuhan ZOU', - author_email='zouyuhan@sensorsdata.cn', - url='http://www.sensorsdata.cn', - license='LICENSE.txt', - packages=['sensorsanalytics'], - description='This is the official Python SDK for Sensors Analytics.', - long_description=open('README.txt').read(), -) +# 读取项目的readme介绍 +with open("README.md", "r") as fh: + long_description = fh.read() +setuptools.setup( + name="SensorsAnalyticsSDK", + version="1.10.1", + author="Jianzhong YUE", # 项目作者 + author_email="yuejianzhong@sensorsdata.cn", + description="This is the official Python SDK for Sensors Analytics.", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/sensorsdata/sa-sdk-python", + packages=setuptools.find_packages(), +) \ No newline at end of file From e12df08d1c91e33894b4cf573a1db9ec676440a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B2=B3=E5=89=91=E9=92=9F?= Date: Tue, 31 Dec 2019 14:50:41 +0800 Subject: [PATCH 11/26] =?UTF-8?q?=E5=88=A0=E9=99=A4=20setup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.py | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 setup.py diff --git a/setup.py b/setup.py deleted file mode 100644 index 4493917..0000000 --- a/setup.py +++ /dev/null @@ -1,16 +0,0 @@ -import setuptools - -# 读取项目的readme介绍 -with open("README.md", "r") as fh: - long_description = fh.read() -setuptools.setup( - name="SensorsAnalyticsSDK", - version="1.10.1", - author="Jianzhong YUE", # 项目作者 - author_email="yuejianzhong@sensorsdata.cn", - description="This is the official Python SDK for Sensors Analytics.", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/sensorsdata/sa-sdk-python", - packages=setuptools.find_packages(), -) \ No newline at end of file From 8b848983dc567f1db5bec71b8ab4b965eec22696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B2=B3=E5=89=91=E9=92=9F?= Date: Thu, 2 Jan 2020 12:46:06 +0800 Subject: [PATCH 12/26] =?UTF-8?q?=E6=81=A2=E5=A4=8D=20setup=20=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 setup.py diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..4493917 --- /dev/null +++ b/setup.py @@ -0,0 +1,16 @@ +import setuptools + +# 读取项目的readme介绍 +with open("README.md", "r") as fh: + long_description = fh.read() +setuptools.setup( + name="SensorsAnalyticsSDK", + version="1.10.1", + author="Jianzhong YUE", # 项目作者 + author_email="yuejianzhong@sensorsdata.cn", + description="This is the official Python SDK for Sensors Analytics.", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/sensorsdata/sa-sdk-python", + packages=setuptools.find_packages(), +) \ No newline at end of file From ecf8772d291568a62302b797727e104029f25390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B2=B3=E5=89=91=E9=92=9F?= Date: Wed, 13 May 2020 14:26:43 +0800 Subject: [PATCH 13/26] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bbc6437..709d6b7 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,24 @@ Once the SDK is successfully installed, use the Sensors Analytics SDK likes: sa.track("ABCDEFG1234567", "ServerStart") ``` -## To learn more +## 讨论 -See our [full manual](http://www.sensorsdata.cn/manual/python_sdk.html) -或者加入神策官方 SDK QQ 讨论群:

-![ QQ 讨论群](https://github.com/sensorsdata/sa-sdk-android/raw/master/docs/qrCode.jpeg) +| 扫码加入神策数据开源社区 QQ 群
群号:785122381 | 扫码加入神策数据开源社区微信群 | +| ------ | ------ | +|![ QQ 讨论群](https://opensource.sensorsdata.cn/wp-content/uploads/ContentCommonPic_1.png) | ![ 微信讨论群 ](https://opensource.sensorsdata.cn/wp-content/uploads/ContentCommonPic_2.png) | + +## 公众号 + +| 扫码关注
神策数据开源社区 | 扫码关注
神策数据开源社区服务号 | +| ------ | ------ | +|![ 微信订阅号 ](https://opensource.sensorsdata.cn/wp-content/uploads/ContentCommonPic_3.png) | ![ 微信服务号 ](https://opensource.sensorsdata.cn/wp-content/uploads/ContentCommonPic_4.png) | + + +## 新书推荐 + +| 《数据驱动:从方法到实践》 | 《Android 全埋点解决方案》 | 《iOS 全埋点解决方案》 +| ------ | ------ | ------ | +| [![《数据驱动:从方法到实践》](https://opensource.sensorsdata.cn/wp-content/uploads/data_driven_book_1.jpg)](https://item.jd.com/12322322.html) | [![《Android 全埋点解决方案》](https://opensource.sensorsdata.cn/wp-content/uploads/Android-全埋点thumbnail_1.png)](https://item.jd.com/12574672.html) | ![《iOS 全埋点解决方案》](https://opensource.sensorsdata.cn/wp-content/uploads/iOS-全埋点thumbnail_1.png) ## License From 638b155a4bda5612a43a95b778e273d42b44b22f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B2=B3=E5=89=91=E9=92=9F?= Date: Mon, 18 May 2020 17:25:33 +0800 Subject: [PATCH 14/26] no message --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 709d6b7..65cf8a3 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Once the SDK is successfully installed, use the Sensors Analytics SDK likes: | 《数据驱动:从方法到实践》 | 《Android 全埋点解决方案》 | 《iOS 全埋点解决方案》 | ------ | ------ | ------ | -| [![《数据驱动:从方法到实践》](https://opensource.sensorsdata.cn/wp-content/uploads/data_driven_book_1.jpg)](https://item.jd.com/12322322.html) | [![《Android 全埋点解决方案》](https://opensource.sensorsdata.cn/wp-content/uploads/Android-全埋点thumbnail_1.png)](https://item.jd.com/12574672.html) | ![《iOS 全埋点解决方案》](https://opensource.sensorsdata.cn/wp-content/uploads/iOS-全埋点thumbnail_1.png) +| [![《数据驱动:从方法到实践》](https://opensource.sensorsdata.cn/wp-content/uploads/data_driven_book_1.jpg)](https://item.jd.com/12322322.html) | [![《Android 全埋点解决方案》](https://opensource.sensorsdata.cn/wp-content/uploads/Android-全埋点thumbnail_1.png)](https://item.jd.com/12574672.html) | [![《iOS 全埋点解决方案》](https://opensource.sensorsdata.cn/wp-content/uploads/iOS-全埋点thumbnail_1.png)](https://item.jd.com/12867068.html) ## License From 6987f74e979a12b0dad99588f4d2cc53d8a6a5d1 Mon Sep 17 00:00:00 2001 From: YueJZSensorsData <36151500+YueJZSensorsData@users.noreply.github.com> Date: Mon, 26 Oct 2020 11:32:11 +0800 Subject: [PATCH 15/26] Update README.md --- README.md | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 65cf8a3..30f1e06 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,29 @@ -# Sensors Analytics +![logo](https://opensource.sensorsdata.cn/wp-content/uploads/logo.png) +

+[![License](https://img.shields.io/github/license/sensorsdata/sa-sdk-cpp.svg)](https://github.com/sensorsdata/sa-sdk-c/blob/master/LICENSE) +[![GitHub release](https://img.shields.io/github/tag/sensorsdata/sa-sdk-c.svg?label=release)](https://github.com/sensorsdata/sa-sdk-c/releases) +[![GitHub release date](https://img.shields.io/github/release-date/sensorsdata/sa-sdk-c.svg)](https://github.com/sensorsdata/sa-sdk-c/releases) -This is the official Python SDK for Sensors Analytics. +## 神策简介 -## Easy Installation +[**神策数据**](https://www.sensorsdata.cn/) +(Sensors Data),隶属于神策网络科技(北京)有限公司,是一家专业的大数据分析服务公司,大数据分析行业开拓者,为客户提供深度用户行为分析平台、以及专业的咨询服务和行业解决方案,致力于帮助客户实现数据驱动。神策数据立足大数据及用户行为分析的技术与实践前沿,业务现已覆盖以互联网、金融、零售快消、高科技、制造等为代表的十多个主要行业、并可支持企业多个职能部门。公司总部在北京,并在上海、深圳、合肥、武汉等地拥有本地化的服务团队,覆盖东区及南区市场;公司拥有专业的服务团队,为客户提供一对一的客户服务。公司在大数据领域积累的核心关键技术,包括在海量数据采集、存储、清洗、分析挖掘、可视化、智能应用、安全与隐私保护等领域。 [**More**](https://www.sensorsdata.cn/about/aboutus.html) -You can get Sensors Analytics SDK using pip. +## SDK 简介 -``` - pip install SensorsAnalyticsSDK -``` +SensorsAnalytics SDK 是国内第一家开源商用版用户行为采集 SDK,目前支持代码埋点、全埋点、App 点击图、可视化全埋点等。目前已累计有 1500 多家付费客户,2500+ 的 App 集成使用,作为 App 数据采集利器,致力于帮助客户挖掘更多的商业价值,为其精准运营和业务支撑提供了可靠的数据来源。其采集全面而灵活、性能良好,并一直保持稳定的迭代,经受住了时间和客户的考验。 -Once the SDK is successfully installed, use the Sensors Analytics SDK likes: +## 基本要求 +SDK 兼容 Python 2.6+ 和 Python3 3.X,不依赖第三方库。 -```python - import sensorsanalytics +## 贡献 - // Gets the url of Sensors Analytics in the home page. - SA_SERVER_URL = 'YOUR_SERVER_URL' +* 1. 在您的 GitHub 账户下 fork sa-sdk-android 开源项目; +* 2. 根据您的需求在本地 clone 一份 sa-sdk-android 源码; +* 3. 您修改或者新增功能后,push 到您 fork 的远程分支; +* 4. 创建 pull request,向 sa-sdk-android 官方开发分支提交合入请求; +* 5. 神策 SDK 研发团队会及时 review 代码,测试通过后合入。 - // Initialized the Sensors Analytics SDK with Default Consumer - consumer = sensorsanalytics.DefaultConsumer(SA_SERVER_URL) - sa = sensorsanalytics.SensorsAnalytics(consumer) - - // Track the event 'ServerStart' - sa.track("ABCDEFG1234567", "ServerStart") -``` ## 讨论 @@ -61,4 +60,4 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -**禁止一切基于神策数据 Python 开源 SDK 的所有商业活动!** \ No newline at end of file +**禁止一切基于神策数据 Python 开源 SDK 的所有商业活动!** From 3152debe3013c807f282292d04361bdbb7177e23 Mon Sep 17 00:00:00 2001 From: YueJZSensorsData <36151500+YueJZSensorsData@users.noreply.github.com> Date: Mon, 26 Oct 2020 11:37:48 +0800 Subject: [PATCH 16/26] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 更新官网文档链接 --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 30f1e06..045949b 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,10 @@ SensorsAnalytics SDK 是国内第一家开源商用版用户行为采集 SDK, ## 基本要求 SDK 兼容 Python 2.6+ 和 Python3 3.X,不依赖第三方库。 +## 集成文档 + +请参考神策官网 [Python SDK 集成文档](https://manual.sensorsdata.cn/sa/latest/tech_sdk_server_python-1573931.html)。 + ## 贡献 * 1. 在您的 GitHub 账户下 fork sa-sdk-android 开源项目; From 488be6c9a301a9f1776f82efc75a723fc2ac6a0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B2=B3=E5=89=91=E9=92=9F?= Date: Wed, 4 Nov 2020 15:56:05 +0800 Subject: [PATCH 17/26] =?UTF-8?q?[feature]=20=E4=BC=98=E5=8C=96=20batch=5F?= =?UTF-8?q?consumer=20=E5=A4=9A=E7=BA=BF=E7=A8=8B=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- sensorsanalytics/sdk.py | 25 ++++++++++++++----------- setup.py | 2 +- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 045949b..463db13 100644 --- a/README.md +++ b/README.md @@ -64,4 +64,4 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -**禁止一切基于神策数据 Python 开源 SDK 的所有商业活动!** +**禁止一切基于神策数据 Python 开源 SDK 的所有商业活动!** \ No newline at end of file diff --git a/sensorsanalytics/sdk.py b/sensorsanalytics/sdk.py index e2a7beb..a5544a7 100644 --- a/sensorsanalytics/sdk.py +++ b/sensorsanalytics/sdk.py @@ -25,7 +25,8 @@ import urllib2 import urllib -SDK_VERSION = '1.10.1' +SDK_VERSION = '1.10.2' +batch_consumer_lock = threading.RLock() try: isinstance("", basestring) @@ -633,21 +634,23 @@ def __init__(self, url_prefix, max_size=50, request_timeout=None): self._max_size = min(50, max_size) def send(self, json_message): - self._buffers.append(json_message) - if len(self._buffers) >= self._max_size: - self.flush() + with batch_consumer_lock: + self._buffers.append(json_message) + if len(self._buffers) >= self._max_size: + self.flush() def flush(self): """ 用户可以主动调用 flush 接口,以便在需要的时候立即进行数据发送。 """ - while self._buffers: - msg_list = self._buffers[:self._max_size] - self._do_request({ - 'data_list': self._encode_msg_list(msg_list), - 'gzip': 1 - }) - self._buffers = self._buffers[self._max_size:] + with batch_consumer_lock: + while self._buffers: + msg_list = self._buffers[:self._max_size] + self._do_request({ + 'data_list': self._encode_msg_list(msg_list), + 'gzip': 1 + }) + self._buffers = self._buffers[self._max_size:] def close(self): """ diff --git a/setup.py b/setup.py index 4493917..97b1cb4 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ long_description = fh.read() setuptools.setup( name="SensorsAnalyticsSDK", - version="1.10.1", + version="1.10.2", author="Jianzhong YUE", # 项目作者 author_email="yuejianzhong@sensorsdata.cn", description="This is the official Python SDK for Sensors Analytics.", From 9ef10c4918350d7e8a99eb1c7c0e9921cca09c02 Mon Sep 17 00:00:00 2001 From: YueJZSensorsData <36151500+YueJZSensorsData@users.noreply.github.com> Date: Tue, 5 Jan 2021 14:30:07 +0800 Subject: [PATCH 18/26] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 463db13..0e2c33a 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ SDK 兼容 Python 2.6+ 和 Python3 3.X,不依赖第三方库。 ## License -Copyright 2015-2020 Sensors Data Inc. +Copyright 2015-2021 Sensors Data Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -64,4 +64,4 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -**禁止一切基于神策数据 Python 开源 SDK 的所有商业活动!** \ No newline at end of file +**禁止一切基于神策数据 Python 开源 SDK 的所有商业活动!** From 40c16db70b7711e16e03343450cbbcf5d95c729c Mon Sep 17 00:00:00 2001 From: YueJZSensorsData <36151500+YueJZSensorsData@users.noreply.github.com> Date: Tue, 5 Jan 2021 14:55:53 +0800 Subject: [PATCH 19/26] Update LICENSE --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index d541085..99a076d 100755 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2015-2020 Sensors Data Inc. + Copyright 2015-2021 Sensors Data Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 4cc95acd984355cefc0681250d6b3ef2bcbea2f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B2=B3=E5=89=91=E9=92=9F?= Date: Wed, 20 Oct 2021 20:43:59 +0800 Subject: [PATCH 20/26] =?UTF-8?q?[fix]=20=E4=BF=AE=E5=A4=8D=20debug=20url?= =?UTF-8?q?=20=E6=88=AA=E5=8F=96=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sensorsanalytics/sdk.py | 7 +++++-- setup.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/sensorsanalytics/sdk.py b/sensorsanalytics/sdk.py index a5544a7..3864db3 100644 --- a/sensorsanalytics/sdk.py +++ b/sensorsanalytics/sdk.py @@ -25,7 +25,7 @@ import urllib2 import urllib -SDK_VERSION = '1.10.2' +SDK_VERSION = '1.10.3' batch_consumer_lock = threading.RLock() try: @@ -789,7 +789,10 @@ def __init__(self, url_prefix, write_data=True, request_timeout=None): """ debug_url = urlparse(url_prefix) ## 将 URI Path 替换成 Debug 模式的 '/debug' - debug_url = debug_url._replace(path = '/debug') + url_path = debug_url.path + index = url_path.rfind('/') + debug_url_path = url_path[0:index] + '/debug' + debug_url = debug_url._replace(path = debug_url_path) self._debug_url_prefix = debug_url.geturl() self._request_timeout = request_timeout diff --git a/setup.py b/setup.py index 97b1cb4..3e6d920 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ long_description = fh.read() setuptools.setup( name="SensorsAnalyticsSDK", - version="1.10.2", + version="1.10.3", author="Jianzhong YUE", # 项目作者 author_email="yuejianzhong@sensorsdata.cn", description="This is the official Python SDK for Sensors Analytics.", From d5341b928723c151e592c4ef9282204a68fca9a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=80=A9?= Date: Mon, 12 Jun 2023 16:50:15 +0800 Subject: [PATCH 21/26] Release 1.11.0 --- LICENSE | 2 +- README.md | 10 +- sensorsanalytics/sdk.py | 382 ++++++++++++++++++++++++++++------- sensorsanalytics/test_sdk.py | 34 +++- setup.py | 2 +- 5 files changed, 352 insertions(+), 78 deletions(-) diff --git a/LICENSE b/LICENSE index 99a076d..ba66130 100755 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2015-2021 Sensors Data Inc. + Copyright 2015-2023 Sensors Data Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 0e2c33a..48ae404 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,10 @@ SDK 兼容 Python 2.6+ 和 Python3 3.X,不依赖第三方库。 ## 贡献 -* 1. 在您的 GitHub 账户下 fork sa-sdk-android 开源项目; -* 2. 根据您的需求在本地 clone 一份 sa-sdk-android 源码; +* 1. 在您的 GitHub 账户下 fork sa-sdk-python 开源项目; +* 2. 根据您的需求在本地 clone 一份 sa-sdk-python 源码; * 3. 您修改或者新增功能后,push 到您 fork 的远程分支; -* 4. 创建 pull request,向 sa-sdk-android 官方开发分支提交合入请求; +* 4. 创建 pull request,向 sa-sdk-python 官方开发分支提交合入请求; * 5. 神策 SDK 研发团队会及时 review 代码,测试通过后合入。 @@ -50,7 +50,7 @@ SDK 兼容 Python 2.6+ 和 Python3 3.X,不依赖第三方库。 ## License -Copyright 2015-2021 Sensors Data Inc. +Copyright 2015-2023 Sensors Data Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -64,4 +64,4 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -**禁止一切基于神策数据 Python 开源 SDK 的所有商业活动!** +**禁止一切基于神策数据 Python 开源 SDK 的所有商业活动!** \ No newline at end of file diff --git a/sensorsanalytics/sdk.py b/sensorsanalytics/sdk.py index 3864db3..75ab62b 100644 --- a/sensorsanalytics/sdk.py +++ b/sensorsanalytics/sdk.py @@ -13,6 +13,7 @@ import threading import time import traceback +from enum import Enum try: from urllib.parse import urlparse @@ -25,11 +26,13 @@ import urllib2 import urllib -SDK_VERSION = '1.10.3' +SDK_VERSION = '1.11.0' batch_consumer_lock = threading.RLock() try: isinstance("", basestring) + + def is_str(s): return isinstance(s, basestring) except NameError: @@ -37,6 +40,8 @@ def is_str(s): return isinstance(s, str) try: isinstance(1, long) + + def is_int(n): return isinstance(n, int) or isinstance(n, long) except NameError: @@ -79,56 +84,60 @@ class SensorsAnalyticsDebugException(Exception): if os.name == 'nt': # pragma: no cover import msvcrt + def lock(file_): try: savepos = file_.tell() - + file_.seek(0) try: msvcrt.locking(file_.fileno(), msvcrt.LK_LOCK, 1) except IOError as e: - raise SensorsAnalyticsFileLockException(e) + raise SensorsAnalyticsFileLockException(e) finally: if savepos: file_.seek(savepos) except IOError as e: - raise SensorsAnalyticsFileLockException(e) + raise SensorsAnalyticsFileLockException(e) + def unlock(file_): try: savepos = file_.tell() if savepos: file_.seek(0) - + try: msvcrt.locking(file_.fileno(), msvcrt.LK_UNLCK, 1) except IOError as e: - raise SensorsAnalyticsFileLockException(e) + raise SensorsAnalyticsFileLockException(e) finally: if savepos: file_.seek(savepos) except IOError as e: - raise SensorsAnalyticsFileLockException(e) + raise SensorsAnalyticsFileLockException(e) elif os.name == 'posix': # pragma: no cover import fcntl + def lock(file_): try: fcntl.flock(file_.fileno(), fcntl.LOCK_EX) except IOError as e: - raise SensorsAnalyticsFileLockException(e) + raise SensorsAnalyticsFileLockException(e) + def unlock(file_): fcntl.flock(file_.fileno(), fcntl.LOCK_UN) else: - raise SensorsAnalyticsFileLockException("SensorsAnalytics SDK is defined for NT and POSIX system.") + raise SensorsAnalyticsFileLockException("SensorsAnalytics SDK is defined for NT and POSIX system.") class SAFileLock(object): - + def __init__(self, file_handler): self._file_handler = file_handler @@ -139,12 +148,15 @@ def __enter__(self): def __exit__(self, t, v, tb): unlock(self._file_handler) + class SensorsAnalytics(object): """ 使用一个 SensorsAnalytics 的实例来进行数据发送。 """ - NAME_PATTERN = re.compile(r"^((?!^distinct_id$|^original_id$|^time$|^properties$|^id$|^first_id$|^second_id$|^users$|^events$|^event$|^user_id$|^date$|^datetime$)[a-zA-Z_$][a-zA-Z\d_$]{0,99})$", re.I) + NAME_PATTERN = re.compile( + r"^((?!^distinct_id$|^original_id$|^time$|^properties$|^id$|^first_id$|^second_id$|^users$|^events$|^event$|^event_id$|^device_id$|^user_id$|^date$|^datetime$|^user_group|^user_tag)[a-zA-Z_$][a-zA-Z\d_$]{0,99})$", + re.I) class DatetimeSerializer(json.JSONEncoder): """ @@ -155,8 +167,8 @@ def default(self, obj): if isinstance(obj, datetime.datetime): head_fmt = "%Y-%m-%d %H:%M:%S" return "{main_part}.{ms_part}".format( - main_part=obj.strftime(head_fmt), - ms_part=int(obj.microsecond/1000)) + main_part=obj.strftime(head_fmt), + ms_part=int(obj.microsecond / 1000)) elif isinstance(obj, datetime.date): fmt = '%Y-%m-%d' return obj.strftime(fmt) @@ -180,7 +192,7 @@ def __init__(self, consumer=None, project_name=None, enable_time_free=False): self._default_project_name = project_name self._enable_time_free = enable_time_free self._super_properties = {} - self.clear_super_properties(); + self.clear_super_properties() @staticmethod def _now(): @@ -190,7 +202,7 @@ def _now(): def _json_dumps(data): return json.dumps(data, separators=(',', ':'), cls=SensorsAnalytics.DatetimeSerializer) - def register_super_properties(self, super_properties): + def register_super_properties(self, super_properties): """ 设置每个事件都带有的一些公共属性,当 track 的 properties 和 super properties 有相同的 key 时,将采用 track 的 @@ -214,12 +226,13 @@ def track(self, distinct_id, event_name, properties=None, is_login_id=False): :param distinct_id: 用户的唯一标识 :param event_name: 事件名称 :param properties: 事件的属性 + :param is_login_id 是否是登陆 ID """ - all_properties = self._super_properties.copy() + all_properties = self._super_properties.copy() if properties: all_properties.update(properties) self._track_event('track', event_name, distinct_id, None, all_properties, is_login_id) - + def track_signup(self, distinct_id, original_id, properties=None): """ 这个接口是一个较为复杂的功能,请在使用前先阅读相关说明:http://www.sensorsdata.cn/manual/track_signup.html, @@ -234,38 +247,47 @@ def track_signup(self, distinct_id, original_id, properties=None): raise SensorsAnalyticsIllegalDataException("property [original_id] must not be empty") if len(str(original_id)) > 255: raise SensorsAnalyticsIllegalDataException("the max length of property [original_id] is 255") - - all_properties = self._super_properties.copy() + + all_properties = self._super_properties.copy() if properties: all_properties.update(properties) - + self._track_event('track_signup', '$SignUp', distinct_id, original_id, all_properties, False) @staticmethod def _normalize_properties(data): if "properties" in data and data["properties"] is not None: for key, value in data["properties"].items(): - if not is_str(key): - raise SensorsAnalyticsIllegalDataException("property key must be a str. [key=%s]" % str(key)) - if len(key) > 255: - raise SensorsAnalyticsIllegalDataException("the max length of property key is 256. [key=%s]" % str(key)) - if not SensorsAnalytics.NAME_PATTERN.match(key): - raise SensorsAnalyticsIllegalDataException( - "property key must be a valid variable name. [key=%s]" % str(key)) + SensorsAnalytics._assert_key(key) + SensorsAnalytics._assert_value(value, key) - if is_str(value) and len(value) > 8191: - raise SensorsAnalyticsIllegalDataException("the max length of property key is 8192. [key=%s]" % str(key)) + @staticmethod + def _assert_key(key): + if not is_str(key): + raise SensorsAnalyticsIllegalDataException("property key must be a str. [key=%s]" % str(key)) + if len(key) > 255: + raise SensorsAnalyticsIllegalDataException( + "the max length of property key is 256. [key=%s]" % str(key)) + if not SensorsAnalytics.NAME_PATTERN.match(key): + raise SensorsAnalyticsIllegalDataException( + "property key must be a valid variable name. [key=%s]" % str(key)) - if not is_str(value) and not is_int(value) and not isinstance(value, float)\ - and not isinstance(value, datetime.datetime) and not isinstance(value, datetime.date)\ - and not isinstance(value, list) and value is not None: + @staticmethod + def _assert_value(value, key=None): + if is_str(value) and len(value) > 8191: + raise SensorsAnalyticsIllegalDataException( + "the max length of property key is 8192. [key=%s]" % str(key)) + + if not is_str(value) and not is_int(value) and not isinstance(value, float) \ + and not isinstance(value, datetime.datetime) and not isinstance(value, datetime.date) \ + and not isinstance(value, list) and value is not None: + raise SensorsAnalyticsIllegalDataException( + "property value must be a str/int/float/datetime/date/list. [value=%s]" % type(value)) + if isinstance(value, list): + for lvalue in value: + if not is_str(lvalue): raise SensorsAnalyticsIllegalDataException( - "property value must be a str/int/float/datetime/date/list. [value=%s]" % type(value)) - if isinstance(value, list): - for lvalue in value: - if not is_str(lvalue): - raise SensorsAnalyticsIllegalDataException( - "[list] property's value must be a str. [value=%s]" % type(lvalue)) + "[list] property's value must be a str. [value=%s]" % type(lvalue)) @staticmethod def _normalize_data(data): @@ -283,7 +305,7 @@ def _normalize_data(data): ts = int(data['time']) ts_num = len(str(ts)) if ts_num < 10 or ts_num > 13: - raise SensorsAnalyticsIllegalDataException("property [time] must be a timestamp in microseconds") + raise SensorsAnalyticsIllegalDataException("property [time] must be a timestamp in microseconds") if ts_num == 10: ts *= 1000 @@ -291,11 +313,13 @@ def _normalize_data(data): # 检查 Event Name if 'event' in data and not SensorsAnalytics.NAME_PATTERN.match(data['event']): - raise SensorsAnalyticsIllegalDataException("event name must be a valid variable name. [name=%s]" % data['event']) + raise SensorsAnalyticsIllegalDataException( + "event name must be a valid variable name. [name=%s]" % data['event']) # 检查 Event Name if 'project' in data and not SensorsAnalytics.NAME_PATTERN.match(data['project']): - raise SensorsAnalyticsIllegalDataException("project name must be a valid variable name. [project=%s]" % data['project']) + raise SensorsAnalyticsIllegalDataException( + "project name must be a valid variable name. [project=%s]" % data['project']) # 检查 properties SensorsAnalytics._normalize_properties(data) @@ -303,12 +327,12 @@ def _normalize_data(data): def _get_lib_properties(self): lib_properties = { - '$lib' : 'python', - '$lib_version' : SDK_VERSION, - '$lib_method' : 'code', + '$lib': 'python', + '$lib_version': SDK_VERSION, + '$lib_method': 'code', } - if '$app_version' in self._super_properties: + if '$app_version' in self._super_properties: lib_properties['$app_version'] = self._super_properties['$app_version'] try: @@ -319,21 +343,22 @@ def _get_lib_properties(self): try: file_name = trace[-4][0] line_number = trace[-4][1] - + if trace[-4][2].startswith('<'): function_name = '' else: function_name = trace[-4][2] - + try: if len(trace) > 4 and trace[-5][3]: - class_name = trace[-5][3].split('(')[0] + class_name = trace[-5][3].split('(')[0] else: class_name = '' except: - print(trace.format()) + print(trace.format()) - lib_properties['$lib_detail'] = '%s##%s##%s##%s' % (class_name, function_name, file_name, line_number) + lib_properties['$lib_detail'] = '%s##%s##%s##%s' % ( + class_name, function_name, file_name, line_number) except: pass @@ -353,7 +378,6 @@ def _get_common_properties(self): return common_properties - @staticmethod def _extract_user_time(properties): """ @@ -393,6 +417,7 @@ def profile_set(self, distinct_id, profiles, is_login_id=False): :param distinct_id: 用户的唯一标识 :param profiles: 用户属性 + :param is_login_id: distinct_id 是否是登陆 id """ return self._track_event('profile_set', None, distinct_id, None, profiles, is_login_id) @@ -402,8 +427,9 @@ def profile_set_once(self, distinct_id, profiles, is_login_id=False): :param distinct_id: 用户的唯一标识 :param profiles: 用户属性 + :param is_login_id: distinct_id 是否是登陆 id """ - return self._track_event('profile_set_once', None, distinct_id, None, profiles, is_login_id) + return self._track_event('profile_set_once', None, distinct_id, None, profiles, is_login_id) def profile_increment(self, distinct_id, profiles, is_login_id=False): """ @@ -411,7 +437,14 @@ def profile_increment(self, distinct_id, profiles, is_login_id=False): :param distinct_id: 用户的唯一标识 :param profiles: 用户属性 + :param is_login_id: distinct_id 是否是登陆 id """ + if isinstance(profiles, dict): + for key, value in profiles.items(): + if not is_int(value): + raise SensorsAnalyticsIllegalDataException("property value must be Number. [key=%s]" % str(key)) + else: + raise SensorsAnalyticsIllegalDataException("profiles must be dict type.") return self._track_event('profile_increment', None, distinct_id, None, profiles, is_login_id) def profile_append(self, distinct_id, profiles, is_login_id=False): @@ -420,7 +453,14 @@ def profile_append(self, distinct_id, profiles, is_login_id=False): :param distinct_id: 用户的唯一标识 :param profiles: 用户属性 + :param is_login_id: distinct_id 是否是登陆 id """ + if isinstance(profiles, dict): + for key, value in profiles.items(): + if not isinstance(value,list): + raise SensorsAnalyticsIllegalDataException("property value must be list. [key=%s]" % str(key)) + else: + raise SensorsAnalyticsIllegalDataException("profiles must be dict type.") return self._track_event('profile_append', None, distinct_id, None, profiles, is_login_id) def profile_unset(self, distinct_id, profile_keys, is_login_id=False): @@ -429,6 +469,7 @@ def profile_unset(self, distinct_id, profile_keys, is_login_id=False): :param distinct_id: 用户的唯一标识 :param profile_keys: 用户属性键值列表 + :param is_login_id: distinct_id 是否是登陆 id """ if isinstance(profile_keys, list): profile_keys = dict((key, True) for key in profile_keys) @@ -439,6 +480,7 @@ def profile_delete(self, distinct_id, is_login_id=False): 删除整个用户的信息。 :param distinct_id: 用户的唯一标识 + :param is_login_id: distinct_id 是否是登陆 id """ return self._track_event('profile_delete', None, distinct_id, None, {}, is_login_id) @@ -448,7 +490,7 @@ def item_set(self, item_type, item_id, properties=None): :param item_type: 物品类型 :param item_id: 物品的唯一标识 - :param profiles: 物品属性 + :param properties: 物品属性 """ return self._track_item('item_set', item_type, item_id, properties) @@ -458,7 +500,7 @@ def item_delete(self, item_type, item_id, properties=None): :param item_type: 物品类型 :param item_id: 物品的唯一标识 - :param profiles: 物品属性 + :param properties: 物品属性 """ return self._track_item('item_delete', item_type, item_id, properties) @@ -502,8 +544,7 @@ def _track_item(self, event_type, item_type, item_id, properties): self._json_dumps(data) self._consumer.send(self._json_dumps(data)) - - def _track_event(self, event_type, event_name, distinct_id, original_id, properties, is_login_id): + def _track_event(self, event_type, event_name, distinct_id, original_id, properties, is_login_id, *identities): event_time = self._extract_user_time(properties) or self._now() event_token = self._extract_token(properties) event_project = self._extract_project(properties) @@ -515,19 +556,28 @@ def _track_event(self, event_type, event_name, distinct_id, original_id, propert 'properties': properties, 'lib': self._get_lib_properties(), } + + if identities: + identities_data = dict() + for identity in identities: + identities_data[identity.key] = identity.value + data["identities"] = identities_data + if self._default_project_name is not None: data['project'] = self._default_project_name - if event_type == "track" or event_type == "track_signup": + if event_type == EventType.TRACK.value or event_type == EventType.TRACK_SIGNUP.value \ + or event_type == EventType.TRACK_ID_BIND.value \ + or event_type == EventType.TRACK_ID_UNBIND.value: data["event"] = event_name - if event_type == "track_signup": + if event_type == EventType.TRACK_SIGNUP.value: data["original_id"] = original_id if self._enable_time_free: data["time_free"] = True - if is_login_id: + if is_login_id or self._is_identity_has_login_id(*identities): properties["$is_login_id"] = True if event_token is not None: @@ -539,6 +589,15 @@ def _track_event(self, event_type, event_name, distinct_id, original_id, propert data = self._normalize_data(data) self._consumer.send(self._json_dumps(data)) + @staticmethod + def _is_identity_has_login_id(*identities): + if not identities: + return False + for identity in identities: + if identity.key == SensorsAnalyticsIdentity.LOGIN_ID: + return True + return False + def flush(self): """ 对于不立即发送数据的 Consumer,调用此接口应当立即进行已有数据的发送。 @@ -552,6 +611,159 @@ def close(self): """ self._consumer.close() + @staticmethod + def _check_identity_type(*identities): + if not identities: + raise SensorsAnalyticsIllegalDataException("Identity (or list) can not be none or empty") + key_repeat_map = {} + duplicate_keys = set() + for identity in identities: + if not isinstance(identity, SensorsAnalyticsIdentity): + raise SensorsAnalyticsIllegalDataException("Identity type must be SensorsAnalyticsIdentity") + SensorsAnalytics._assert_key(identity.key) + if not is_str(identity.value): + raise SensorsAnalyticsIllegalDataException("identity value must be a str. [key=%s]" % str(identity.key)) + if not len(identity.value.strip()): + raise SensorsAnalyticsIllegalDataException("identity value is empty. [key=%s]" % str(identity.key)) + if len(identity.value) > 255: + raise SensorsAnalyticsIllegalDataException( + "the max length of property value is 256. [key=%s]" % str(identity.key)) + count = key_repeat_map.get(identity.key, 0) + count += 1 + key_repeat_map[identity.key] = count + if count > 1: + duplicate_keys.add(identity.key) + if duplicate_keys: + raise SensorsAnalyticsIllegalDataException("Identity has duplicate key. [key=%s]" % str(duplicate_keys)) + + @staticmethod + def _get_distinct_id(*identities): + if not identities: + return None + distinct_id = "%s+%s" % (identities[0].key, identities[0].value) + for identity in identities: + if SensorsAnalyticsIdentity.LOGIN_ID == identity.key: + distinct_id = identity.value + break + return distinct_id + + def bind(self, first_identity, second_identity, *other_identities): + """ + 绑定用户标识。至少需要提供两个用户标识信息。identity 的数据类型为 SensorsAnalyticsIdentity + + :param first_identity 待绑定的用户标识, + :param second_identity 待绑定的用户标识 + :param other_identities 其他需要绑定的用户标识 + """ + SensorsAnalytics._check_identity_type(first_identity, second_identity, *other_identities) + all_properties = self._super_properties.copy() + self._track_event(EventType.TRACK_ID_BIND.value, "$BindID", + SensorsAnalytics._get_distinct_id(first_identity, second_identity, *other_identities), + None, all_properties, None, first_identity, second_identity, *other_identities) + + def unbind(self, identity): + """ + 解绑用户标识 + :param identity SensorsAnalyticsIdentity + """ + SensorsAnalytics._check_identity_type(identity) + # if identity.key == SensorsAnalyticsIdentity.LOGIN_ID: + # raise SensorsAnalyticsIllegalDataException("Can not unbind login id.") + all_properties = self._super_properties.copy() + self._track_event(EventType.TRACK_ID_UNBIND.value, "$UnbindID", SensorsAnalytics._get_distinct_id(identity), + None, all_properties, None, identity) + + def track_by_id(self, event_name, properties, *identities): + """ + 使用用户标识 3.0 进行事件埋点 + :param event_name 事件名 + :param properties 事件属性,数据类型为 dict + :param identities 用户标识 + """ + SensorsAnalytics._check_identity_type(*identities) + all_properties = self._super_properties.copy() + if properties: + if not isinstance(properties, dict): + raise SensorsAnalyticsIllegalDataException("properties must be a dict type.") + all_properties.update(properties) + self._track_event(EventType.TRACK.value, event_name, + SensorsAnalytics._get_distinct_id(*identities), + None, all_properties, None, *identities) + + def profile_set_by_id(self, properties, *identities): + """ + 设置用户的属性。如果要设置的 properties 的 key,之前在这个用户的 profile 中已经存在,则覆盖,否则,新创建 + :param properties 用户属性 + :param identities 用户标识,类型是 SensorsAnalyticsIdentity + """ + self._profile_options_by_id(EventType.PROFILE_SET, properties, *identities) + + def profile_set_once_by_id(self, properties, *identities): + """ + 首次设置用户的属性。与 profile_set_by_id 接口不同的是:如果被设置的用户属性已存在,则这条记录会被忽略而不会覆盖已有数据,如果属性不存在则会自动创建 + :param properties 用户属性 + :param identities 用户标识,类型是 SensorsAnalyticsIdentity + """ + self._profile_options_by_id(EventType.PROFILE_SET_ONCE, properties, *identities) + + def profile_unset_by_id(self, profile_keys, *identities): + """ + 删除用户某一个属性 + :param profile_keys: 用户属性键值列表 + :param identities 用户标识,类型是 SensorsAnalyticsIdentity + """ + if isinstance(profile_keys, list): + profile_keys = dict((key, True) for key in profile_keys) + else: + raise SensorsAnalyticsIllegalDataException("profile_keys must be a list.") + self._profile_options_by_id(EventType.PROFILE_UNSET, profile_keys, *identities) + + def profile_append_by_id(self, profiles, *identities): + """ + 追加一个用户的某一个或者多个集合类型的 Profile。 + :param profiles 用户属性,其 key 必须是 str 类型,value 必须是 str 集合类型 + :param identities 用户标识,类型是 SensorsAnalyticsIdentity + """ + if isinstance(profiles, dict): + for key, value in profiles.items(): + if not isinstance(value, list): + raise SensorsAnalyticsIllegalDataException("property value must be list. [key=%s]" % str(key)) + else: + raise SensorsAnalyticsIllegalDataException("profiles must be dict type.") + self._profile_options_by_id(EventType.PROFILE_APPEND, profiles, *identities) + + def profile_delete_by_id(self, *identities): + """ + 删除用户的所有属性 + :param identities 用户标识,类型是 SensorsAnalyticsIdentity + """ + self._profile_options_by_id(EventType.PROFILE_DELETE, {}, *identities) + + def profile_increment_by_id(self, profiles, *identities): + """ + 为用户的一个或多个数值类型的属性累加一个数值,若该属性不存在,则创建它并设置默认值为 0。属性取值只接受 Number类型。 + :param profiles 用户属性,类型是 dict,value 必须是 Number 类型 + :param identities 用户标识,可以是 identity、list、tuple + """ + if isinstance(profiles, dict): + for key, value in profiles.items(): + if not is_int(value): + raise SensorsAnalyticsIllegalDataException("property value must be Number. [key=%s]" % str(key)) + else: + raise SensorsAnalyticsIllegalDataException("profiles must be dict type.") + self._profile_options_by_id(EventType.PROFILE_INCREMENT, profiles, *identities) + + def _profile_options_by_id(self, event_type, properties, *identities): + if not (event_type == EventType.PROFILE_DELETE): + if not properties: + raise SensorsAnalyticsIllegalDataException("Properties can not be None or Empty") + if not isinstance(properties, dict): + raise SensorsAnalyticsIllegalDataException("Properties type must be dict") + SensorsAnalytics._check_identity_type(*identities) + self._track_event(event_type.value, None, SensorsAnalytics._get_distinct_id(*identities), None, properties, + None, *identities) + + class DefaultConsumer(object): """ 默认的 Consumer实现,逐条、同步的发送数据给接收服务器。 @@ -792,8 +1004,8 @@ def __init__(self, url_prefix, write_data=True, request_timeout=None): url_path = debug_url.path index = url_path.rfind('/') debug_url_path = url_path[0:index] + '/debug' - debug_url = debug_url._replace(path = debug_url_path) - + debug_url = debug_url._replace(path=debug_url_path) + self._debug_url_prefix = debug_url.geturl() self._request_timeout = request_timeout self._debug_write_data = write_data @@ -819,7 +1031,7 @@ def _do_request(self, data): encoded_data = urllib.urlencode(data).encode('utf8') try: request = urllib2.Request(self._debug_url_prefix, encoded_data) - if not self._debug_write_data: # 说明只检查,不真正写入数据 + if not self._debug_write_data: # 说明只检查,不真正写入数据 request.add_header('Dry-Run', 'true') if self._request_timeout is not None: response = urllib2.urlopen(request, timeout=self._request_timeout) @@ -896,6 +1108,7 @@ def flush(self): def close(self): self.logger.handlers[0].close() + class ConcurrentLoggingConsumer(object): """ 将数据输出到指定路径,并按天切割,支持多进程并行输出到同一个文件名 @@ -911,7 +1124,7 @@ def close(self): self._file.close() def isValid(self, filename): - return self._filename == filename + return self._filename == filename def write(self, messages): with SAFileLock(self._file): @@ -922,7 +1135,7 @@ def write(self, messages): @classmethod def construct_filename(cls, prefix): - return prefix + '.' + datetime.datetime.now().strftime('%Y-%m-%d') + return prefix + '.' + datetime.datetime.now().strftime('%Y-%m-%d') def __init__(self, prefix, bufferSize=8192): self._prefix = prefix @@ -946,7 +1159,7 @@ def send(self, msg): if len(self._buffer) > self._bufferSize: messages = self._buffer - filename = ConcurrentLoggingConsumer.construct_filename(self._prefix) + filename = ConcurrentLoggingConsumer.construct_filename(self._prefix) if not self._writer.isValid(filename): self._writer.close() self._writer = ConcurrentLoggingConsumer.ConcurrentFileWriter(filename) @@ -954,7 +1167,7 @@ def send(self, msg): self._buffer = [] self._mutex.put(1) - + if messages: self._writer.write(messages) @@ -966,7 +1179,7 @@ def flush(self): if len(self._buffer) > 0: messages = self._buffer - filename = ConcurrentLoggingConsumer.construct_filename(self._prefix) + filename = ConcurrentLoggingConsumer.construct_filename(self._prefix) if not self._writer.isValid(filename): self._writer.close() self._writer = ConcurrentLoggingConsumer.ConcurrentFileWriter(filename) @@ -974,10 +1187,43 @@ def flush(self): self._buffer = [] self._mutex.put(1) - + if messages: self._writer.write(messages) def close(self): self.flush() self._writer.close() + + +# ID-Mapping 3 业务逻辑 +class SensorsAnalyticsIdentity(object): + LOGIN_ID = "$identity_login_id" + """ + 用户登录 id + """ + MOBILE = "$identity_mobile" + """ + 手机号 + """ + EMAIL = "$identity_email" + """ + 邮箱 + """ + + def __init__(self, key, value): + self.key = key + self.value = value + + +class EventType(Enum): + TRACK = "track" + TRACK_SIGNUP = "track_signup" + TRACK_ID_BIND = "track_id_bind" + TRACK_ID_UNBIND = "track_id_unbind" + PROFILE_SET = "profile_set" + PROFILE_SET_ONCE = "profile_set_once" + PROFILE_UNSET = "profile_unset" + PROFILE_APPEND = "profile_append" + PROFILE_DELETE = "profile_delete" + PROFILE_INCREMENT = "profile_increment" diff --git a/sensorsanalytics/test_sdk.py b/sensorsanalytics/test_sdk.py index ddd95e2..a5cd4f2 100644 --- a/sensorsanalytics/test_sdk.py +++ b/sensorsanalytics/test_sdk.py @@ -127,7 +127,7 @@ def testDefaultConsumer(self): sa.track('1234', 'Test', {'From': 'Baidu'}) sa.track_signup('1234', 'abcd', {'Channel': 'Hongbao'}) sa.profile_delete('1234') - sa.profile_append('1234', {'Gender': 'Male'}) + sa.profile_append('1234', {'Gender': ['Male']}) sa.profile_increment('1234', {'CardNum': 1}) sa.profile_set('1234', {'City': '北京'}) sa.profile_unset('1234', ['City']) @@ -141,7 +141,7 @@ def testBatchConsumer(self): sa.track('1234', 'Test', {'From': 'Baidu'}) sa.track_signup('1234', 'abcd', {'Channel': 'Hongbao'}) sa.profile_delete('1234') - sa.profile_append('1234', {'Gender': 'Male'}) + sa.profile_append('1234', {'Gender': ['Male']}) self.assertEqual(self.msg_counter, 0) sa.profile_increment('1234', {'CardNum': 1}) self.assertEqual(self.msg_counter, 5) @@ -161,7 +161,7 @@ def testAsyncBatchConsumer(self): sa.track('1234', 'Test', {'From': 'Baidu'}) sa.track_signup('1234', 'abcd', {'Channel': 'Hongbao'}) sa.profile_delete('1234') - sa.profile_append('1234', {'Gender': 'Male'}) + sa.profile_append('1234', {'Gender': ["male", "femal"]}) self.assertEqual(self.msg_counter, 0) sa.profile_increment('1234', {'CardNum': 1}) time.sleep(0.1) @@ -180,6 +180,34 @@ def testAsyncBatchConsumer(self): time.sleep(0.1) self.assertEqual(self.msg_counter, 9) + def testIDM3(self): + consumer = DefaultConsumer(TEST_URL_PREFIX) + # consumer._do_request = self.mock_request + sa = SensorsAnalytics(consumer) + + # sa.bind(SensorsAnalyticsIdentity("s1", "sv1"), SensorsAnalyticsIdentity("s2", "sv2"), SensorsAnalyticsIdentity("s3", "sv3")) + # sa.bind(SensorsAnalyticsIdentity("s1", "sv1"), SensorsAnalyticsIdentity("s2", "sv2"), SensorsAnalyticsIdentity(SensorsAnalyticsIdentity.LOGIN_ID, "sv3")) + # sa.unbind(SensorsAnalyticsIdentity(SensorsAnalyticsIdentity.EMAIL, "sv1")) + + # sa.track_by_id("hello", None, SensorsAnalyticsIdentity(SensorsAnalyticsIdentity.LOGIN_ID, "sv1"), SensorsAnalyticsIdentity("s2", "sv2")) + + # sa.profile_set_by_id({"p1": "v1"}, SensorsAnalyticsIdentity("s1", "sv1")) + # sa.profile_unset_by_id(["k1", "k2"], SensorsAnalyticsIdentity("s1", "sv1")) + + # sa.profile_append("sss", {"k1": "ss"}, False) + + # sa.profile_append_by_id({"k1": ["a1", "a2", "a3"]}, SensorsAnalyticsIdentity("s1", "sv1"), + # SensorsAnalyticsIdentity("s2", "sv2")) + + # sa.profile_delete_by_id(SensorsAnalyticsIdentity("s1", "sv1"), + # SensorsAnalyticsIdentity("s2", "sv2")) + + # sa.profile_increment_by_id({"age": "123"}, SensorsAnalyticsIdentity("s1", "sv1")) + + sa.flush() + time.sleep(2) + pass + if __name__ == '__main__': unittest.main() diff --git a/setup.py b/setup.py index 3e6d920..828babb 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ long_description = fh.read() setuptools.setup( name="SensorsAnalyticsSDK", - version="1.10.3", + version="1.11.0", author="Jianzhong YUE", # 项目作者 author_email="yuejianzhong@sensorsdata.cn", description="This is the official Python SDK for Sensors Analytics.", From 5a392ccf3c2a4def80935bd5f6e521679e5554ea Mon Sep 17 00:00:00 2001 From: wangzhzh Date: Sun, 7 Apr 2024 11:56:55 +0800 Subject: [PATCH 22/26] Update README.md --- README.md | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 48ae404..e542f03 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -![logo](https://opensource.sensorsdata.cn/wp-content/uploads/logo.png) -

+ + [![License](https://img.shields.io/github/license/sensorsdata/sa-sdk-cpp.svg)](https://github.com/sensorsdata/sa-sdk-c/blob/master/LICENSE) [![GitHub release](https://img.shields.io/github/tag/sensorsdata/sa-sdk-c.svg?label=release)](https://github.com/sensorsdata/sa-sdk-c/releases) [![GitHub release date](https://img.shields.io/github/release-date/sensorsdata/sa-sdk-c.svg)](https://github.com/sensorsdata/sa-sdk-c/releases) @@ -29,28 +29,14 @@ SDK 兼容 Python 2.6+ 和 Python3 3.X,不依赖第三方库。 * 5. 神策 SDK 研发团队会及时 review 代码,测试通过后合入。 -## 讨论 - -| 扫码加入神策数据开源社区 QQ 群
群号:785122381 | 扫码加入神策数据开源社区微信群 | -| ------ | ------ | -|![ QQ 讨论群](https://opensource.sensorsdata.cn/wp-content/uploads/ContentCommonPic_1.png) | ![ 微信讨论群 ](https://opensource.sensorsdata.cn/wp-content/uploads/ContentCommonPic_2.png) | - -## 公众号 - -| 扫码关注
神策数据开源社区 | 扫码关注
神策数据开源社区服务号 | -| ------ | ------ | -|![ 微信订阅号 ](https://opensource.sensorsdata.cn/wp-content/uploads/ContentCommonPic_3.png) | ![ 微信服务号 ](https://opensource.sensorsdata.cn/wp-content/uploads/ContentCommonPic_4.png) | - - ## 新书推荐 -| 《数据驱动:从方法到实践》 | 《Android 全埋点解决方案》 | 《iOS 全埋点解决方案》 +| [《数据驱动:从方法到实践》](https://item.jd.com/12322322.html) | [《Android 全埋点解决方案》](https://item.jd.com/12574672.html) | [《iOS 全埋点解决方案》](https://item.jd.com/12867068.html) | ------ | ------ | ------ | -| [![《数据驱动:从方法到实践》](https://opensource.sensorsdata.cn/wp-content/uploads/data_driven_book_1.jpg)](https://item.jd.com/12322322.html) | [![《Android 全埋点解决方案》](https://opensource.sensorsdata.cn/wp-content/uploads/Android-全埋点thumbnail_1.png)](https://item.jd.com/12574672.html) | [![《iOS 全埋点解决方案》](https://opensource.sensorsdata.cn/wp-content/uploads/iOS-全埋点thumbnail_1.png)](https://item.jd.com/12867068.html) ## License -Copyright 2015-2023 Sensors Data Inc. +Copyright 2015-2024 Sensors Data Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -64,4 +50,4 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -**禁止一切基于神策数据 Python 开源 SDK 的所有商业活动!** \ No newline at end of file +**禁止一切基于神策数据 Python 开源 SDK 的所有商业活动!** From 4c4339dcd95ce54a6f53d90d96d3931398c066b3 Mon Sep 17 00:00:00 2001 From: Exploring Date: Tue, 3 Sep 2024 14:49:20 +0800 Subject: [PATCH 23/26] Update LICENSE --- LICENSE | 230 +++++++------------------------------------------------- 1 file changed, 28 insertions(+), 202 deletions(-) diff --git a/LICENSE b/LICENSE index ba66130..fe748e1 100755 --- a/LICENSE +++ b/LICENSE @@ -1,202 +1,28 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2015-2023 Sensors Data Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - +软件名称:神策分析 SDK +版本号:所有版本 +许可协议版本:1.0 + +1. 商业许可协议(用于商业用途需购买许可) +任何商业用途必须获得商业许可。 + +商业许可协议条款: + +- 商业用途:任何直接或间接产生收入的用途都需要购买商业许可。 +- 付款条款:在使用本软件用于商业用途之前,您必须支付全额许可费用。具体的付款方式将在双方联系后提供。 +- 商业支持:购买商业许可后,您将获得一年的技术支持和软件更新服务。 +- 禁止再许可:商业用户不得再许可、转售或转让本软件。每份商业许可仅适用于单一实体或公司。 +- 源代码访问:购买商业许可的用户将获得本软件的代码访问权限,并可根据业务需求进行内部修改。但不得公开发布或再分发修改后的版本。 +- 使用范围限制:商业许可仅限于购买者的内部使用,不得与第三方共享或用于为第三方提供服务。任何超出许可范围的使用行为均需额外授权,并可能产生额外费用。 +- 联系信息:如需购买商业许可,请联系 weizhangxiang@sensorsdata.com。 +- 知识产权声明:本软件的版权归神策网络科技(北京)有限公司所有。购买商业许可仅授予您使用权,所有权仍归属本公司。 +- 终止条款: 如果您未支付相关费用或违反本协议的任何条款,商业许可将自动终止。您必须立即停止所有商业用途,并销毁或删除所有软件副本。 + +2. 附加授权规则条款 +授权规则条款: + +- 功能限制:未经本软件作者的明确书面许可,您不得移除、绕过或规避本软件中的任何功能限制或试用限制。 +- 商标使用:未经授权,您不得在宣传、市场推广或销售产品时使用本软件的名称、商标或品牌标识。任何商标使用必须得到明确的书面许可。 +- 修改条款:本协议的条款可能会不时更新,用户有责任定期检查最新版本。任何重大更改将通过项目主页或电子邮件通知用户。 + +3. 联系方式 +如需更多信息或申请商业许可,请联系 weizhangxiang@sensorsdata.com。 From fe64c26b13b33536121955da5fad30f68d5e219f Mon Sep 17 00:00:00 2001 From: Exploring Date: Tue, 3 Sep 2024 14:50:12 +0800 Subject: [PATCH 24/26] Update README.md --- README.md | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/README.md b/README.md index e542f03..1bd003f 100644 --- a/README.md +++ b/README.md @@ -20,14 +20,6 @@ SDK 兼容 Python 2.6+ 和 Python3 3.X,不依赖第三方库。 请参考神策官网 [Python SDK 集成文档](https://manual.sensorsdata.cn/sa/latest/tech_sdk_server_python-1573931.html)。 -## 贡献 - -* 1. 在您的 GitHub 账户下 fork sa-sdk-python 开源项目; -* 2. 根据您的需求在本地 clone 一份 sa-sdk-python 源码; -* 3. 您修改或者新增功能后,push 到您 fork 的远程分支; -* 4. 创建 pull request,向 sa-sdk-python 官方开发分支提交合入请求; -* 5. 神策 SDK 研发团队会及时 review 代码,测试通过后合入。 - ## 新书推荐 @@ -35,19 +27,4 @@ SDK 兼容 Python 2.6+ 和 Python3 3.X,不依赖第三方库。 | ------ | ------ | ------ | ## License - -Copyright 2015-2024 Sensors Data Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -**禁止一切基于神策数据 Python 开源 SDK 的所有商业活动!** +[License 协议](https://github.com/sensorsdata/sa-sdk-python/blob/master/LICENSE) From 514ea932e3345c4a0385d110ce4da5b483ada7a2 Mon Sep 17 00:00:00 2001 From: Exploring Date: Wed, 4 Sep 2024 15:16:01 +0800 Subject: [PATCH 25/26] Update LICENSE --- LICENSE | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index fe748e1..0d8fbcb 100755 --- a/LICENSE +++ b/LICENSE @@ -13,7 +13,7 @@ - 禁止再许可:商业用户不得再许可、转售或转让本软件。每份商业许可仅适用于单一实体或公司。 - 源代码访问:购买商业许可的用户将获得本软件的代码访问权限,并可根据业务需求进行内部修改。但不得公开发布或再分发修改后的版本。 - 使用范围限制:商业许可仅限于购买者的内部使用,不得与第三方共享或用于为第三方提供服务。任何超出许可范围的使用行为均需额外授权,并可能产生额外费用。 -- 联系信息:如需购买商业许可,请联系 weizhangxiang@sensorsdata.com。 +- 联系信息:如需购买商业许可,请联系 dv@sensorsdata.com。 - 知识产权声明:本软件的版权归神策网络科技(北京)有限公司所有。购买商业许可仅授予您使用权,所有权仍归属本公司。 - 终止条款: 如果您未支付相关费用或违反本协议的任何条款,商业许可将自动终止。您必须立即停止所有商业用途,并销毁或删除所有软件副本。 @@ -25,4 +25,4 @@ - 修改条款:本协议的条款可能会不时更新,用户有责任定期检查最新版本。任何重大更改将通过项目主页或电子邮件通知用户。 3. 联系方式 -如需更多信息或申请商业许可,请联系 weizhangxiang@sensorsdata.com。 +如需更多信息或申请商业许可,请联系 dv@sensorsdata.com。 From a1f953ea07cb5b41cfe49dfa588eb9b745a49385 Mon Sep 17 00:00:00 2001 From: wangzhzh Date: Fri, 18 Jul 2025 16:41:03 +0800 Subject: [PATCH 26/26] Update README.md --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 1bd003f..06fb0cc 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,15 @@ SensorsAnalytics SDK 是国内第一家开源商用版用户行为采集 SDK,目前支持代码埋点、全埋点、App 点击图、可视化全埋点等。目前已累计有 1500 多家付费客户,2500+ 的 App 集成使用,作为 App 数据采集利器,致力于帮助客户挖掘更多的商业价值,为其精准运营和业务支撑提供了可靠的数据来源。其采集全面而灵活、性能良好,并一直保持稳定的迭代,经受住了时间和客户的考验。 +## 神策埋点 SDK 官网 +如需了解神策埋点 SDK 的更多商业授权信息,请访问[神策埋点 SDK 官网](https://jssdk.debugbox.sensorsdata.cn/)获取更多详细信息。 + +## 联系我们 +若您有商业合作或产品集成需求,请通过下面的渠道联系我们获取专业服务与支持。 + +| 加微信号:skycode008,或扫码添加联系人 | 扫码关注「神策埋点 SDK」公众号 ![gzh](https://github.com/sensorsdata/sa-sdk-android/blob/master/gzh.jpeg) | +| ------ | ------ | + ## 基本要求 SDK 兼容 Python 2.6+ 和 Python3 3.X,不依赖第三方库。