Skip to content

Commit e4c8db6

Browse files
authored
feat: add track event api (#22)
✨ feat: add track event api
1 parent 8d7a5ae commit e4c8db6

8 files changed

Lines changed: 174 additions & 48 deletions

File tree

demo.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import logging
22

33
import featureprobe as fp
4+
import random
5+
import time
46

57
logging.basicConfig(level=logging.WARNING)
68

@@ -14,19 +16,17 @@
1416
start_wait=5)
1517

1618
# Server Side SDK Key for your project and environment
17-
SDK_KEY = 'server-8ed48815ef044428826787e9a238b9c6a479f98c'
19+
SDK_KEY = 'server-9e53c5db4fd75049a69df8881f3bc90edd58fb06'
1820
with fp.Client(SDK_KEY, config) as client:
19-
if client.initialized():
20-
print("SDK successfully initialized!")
21-
else:
21+
if not client.initialized():
2222
print("SDK failed to initialize!")
23-
exit()
2423
# Create one user
2524
# "userId" is used in rules, should be filled in.
2625
user = fp.User().with_attr('userId', '00001')
2726

2827
# Get toggle result for this user.
29-
TOGGLE_KEY = 'campaign_allow_list'
28+
#TOGGLE_KEY = 'campaign_allow_list'
29+
TOGGLE_KEY = 'Event_Analysis'
3030

3131
# Get toggle result for this user
3232
is_open = client.value(TOGGLE_KEY, user, default=False)
@@ -36,3 +36,13 @@
3636
is_open_detail = client.value_detail(TOGGLE_KEY, user, default=False)
3737
print('detail: ' + str(is_open_detail.reason))
3838
print('rule index: ' + str(is_open_detail.rule_index))
39+
40+
# Simulate conversion rate of 1000 users for a new feature
41+
#YOU_EVENT_NAME = "new_feature_conversion";
42+
YOU_EVENT_NAME = "multi_feature"
43+
for i in range(1000):
44+
event_user = fp.User().stable_rollout(str(time.time_ns() + i))
45+
new_feature = client.value(TOGGLE_KEY, event_user, '1')
46+
client.track(YOU_EVENT_NAME, event_user, random.randint(0, 99))
47+
print("New feature conversion.")
48+
time.sleep(0.2)

featureprobe/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
from featureprobe.access_recorder import (
2929
AccessCounter,
30-
AccessRecorder,
30+
AccessSummaryRecorder,
3131
)
3232

3333
from featureprobe.config import Config
@@ -64,7 +64,7 @@
6464
# featureprobe
6565

6666
'AccessCounter',
67-
'AccessRecorder',
67+
'AccessSummaryRecorder',
6868
'Client',
6969
'Config',
7070
'Context',

featureprobe/access_recorder.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222

2323
class AccessCounter:
24-
def __init__(self, value: str, version: int, index: int):
24+
def __init__(self, value: object, version: int, index: int):
2525
self._VALUE = value
2626
self._VERSION = version
2727
self._INDEX = index
@@ -59,12 +59,11 @@ def increment(self):
5959
self._count += 1
6060

6161
def is_group(self, event: "AccessEvent"):
62-
return self._VALUE == event.value \
63-
and self._VERSION == event.version \
64-
and self._INDEX == event.index
62+
return self._VERSION == event.version \
63+
and self._INDEX == event.variation_index
6564

6665

67-
class AccessRecorder:
66+
class AccessSummaryRecorder:
6867
def __init__(self):
6968
self._counters = {} # Dict[str, List[AccessCounter]]
7069
self._start_time = 0
@@ -102,13 +101,13 @@ def add(self, _event: "AccessEvent"): # sourcery skip: use-named-expression
102101
AccessCounter(
103102
_event.value,
104103
_event.version,
105-
_event.index))
104+
_event.variation_index))
106105
else:
107106
groups = [
108107
AccessCounter(
109108
_event.value,
110109
_event.version,
111-
_event.index)]
110+
_event.variation_index)]
112111
self._counters[_event.key] = groups
113112

114113
def snapshot(self):

featureprobe/client.py

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from typing import Optional
1516
import logging
1617
import time
1718
from threading import Event
@@ -20,7 +21,7 @@
2021
from featureprobe.config import Config
2122
from featureprobe.context import Context
2223
from featureprobe.detail import Detail
23-
from featureprobe.event import AccessEvent
24+
from featureprobe.event import AccessEvent, CustomEvent
2425
from featureprobe.internal.empty_str import empty_str
2526
from featureprobe.user import User
2627

@@ -105,12 +106,17 @@ def value(self, toggle_key: str, user: User, default) -> Any:
105106
return default
106107

107108
eval_result = toggle.eval(user, segments, default)
108-
access_event = AccessEvent(timestamp=int(time.time() * 1000),
109-
user=user,
110-
key=toggle_key,
111-
value=str(eval_result.value),
112-
version=eval_result.version,
113-
index=eval_result.variation_index)
109+
access_event = AccessEvent(
110+
timestamp=int(
111+
time.time() * 1000),
112+
user=user,
113+
key=toggle_key,
114+
value=eval_result.value,
115+
version=eval_result.version,
116+
variation_index=eval_result.variation_index,
117+
rule_index=eval_result.rule_index,
118+
reason=eval_result.reason,
119+
track_access_events=toggle.track_access_events)
114120
self._event_processor.push(access_event)
115121
return eval_result.value
116122

@@ -140,11 +146,35 @@ def value_detail(self, toggle_key: str, user: User, default) -> Detail:
140146
reason=eval_result.reason,
141147
rule_index=eval_result.rule_index,
142148
version=eval_result.version)
143-
access_event = AccessEvent(timestamp=int(time.time() * 1000),
144-
user=user,
145-
key=toggle_key,
146-
value=eval_result.value,
147-
version=eval_result.version,
148-
index=eval_result.variation_index)
149+
access_event = AccessEvent(
150+
timestamp=int(
151+
time.time() * 1000),
152+
user=user,
153+
key=toggle_key,
154+
value=eval_result.value,
155+
version=eval_result.version,
156+
variation_index=eval_result.variation_index,
157+
rule_index=eval_result.rule_index,
158+
reason=eval_result.reason,
159+
track_access_events=toggle.track_access_events)
149160
self._event_processor.push(access_event)
150161
return detail
162+
163+
def track(
164+
self,
165+
event_name: str,
166+
user: User,
167+
value: Optional[float] = None):
168+
"""Tracks that a custom defined event
169+
170+
:param event_name: the name of the event.
171+
:param user: :obj:`~featureprobe.User` to be evaluated.
172+
:param value: a numeric value(Optional).
173+
"""
174+
self._event_processor.push(
175+
CustomEvent(
176+
timestamp=int(
177+
time.time() * 1000),
178+
name=event_name,
179+
user=user,
180+
value=value))

featureprobe/default_event_processor.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
import contextlib
16+
import json
1617
import logging
1718
import queue
1819
import threading
@@ -26,9 +27,9 @@
2627
from apscheduler.schedulers.background import BackgroundScheduler
2728
from requests import Session, HTTPError
2829

29-
from featureprobe.access_recorder import AccessRecorder
30+
from featureprobe.access_recorder import AccessSummaryRecorder
3031
from featureprobe.context import Context
31-
from featureprobe.event import Event, AccessEvent
32+
from featureprobe.event import CustomEvent, Event, AccessEvent
3233
from featureprobe.event_processor import EventProcessor
3334

3435

@@ -46,13 +47,13 @@ def __init__(self, _type: Type, event: Optional[Event]):
4647
class EventRepository:
4748
def __init__(self):
4849
self.events = []
49-
self.access = AccessRecorder()
50+
self.access = AccessSummaryRecorder()
5051

5152
@classmethod
5253
def _clone(
5354
cls,
5455
events: List[Event],
55-
access: AccessRecorder) -> "EventRepository":
56+
access: AccessSummaryRecorder) -> "EventRepository":
5657
repo = cls()
5758
repo.events = events.copy()
5859
repo.access = access.snapshot()
@@ -73,6 +74,10 @@ def is_empty(self):
7374
def add(self, event: Event):
7475
if isinstance(event, AccessEvent):
7576
self.access.add(event)
77+
if event.track_access_events:
78+
self.events.append(event)
79+
elif isinstance(event, CustomEvent):
80+
self.events.append(event)
7681

7782
def snapshot(self):
7883
return EventRepository._clone(self.events, self.access)
@@ -185,7 +190,9 @@ def _send_events(self, repositories: List[EventRepository]):
185190
json=repositories,
186191
timeout=self._timeout)
187192
# sourcery skip: replace-interpolation-with-fstring
188-
self._logger.debug('Http response: %s' % resp.text)
193+
self._logger.debug(
194+
'Http request %s, response %s' %
195+
(repositories, resp))
189196
try:
190197
resp.raise_for_status()
191198
except HTTPError as e:

featureprobe/event.py

Lines changed: 73 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,23 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from typing import TYPE_CHECKING
15+
from typing import TYPE_CHECKING, Optional
1616

1717
if TYPE_CHECKING:
1818
from featureprobe.user import User
1919

2020

2121
class Event:
22-
def __init__(self, created_time: int, user: "User"):
22+
def __init__(self, kind: str, created_time: int, user: "User"):
2323
self._created_time = created_time
2424
self._user = user
25+
self._kind = kind
2526

2627
def to_dict(self) -> dict:
2728
return {
28-
'createdTime': self._created_time,
29-
'user': self._user.to_dict(),
29+
'kind': self.kind,
30+
'time': self._created_time,
31+
'user': self._user.key
3032
}
3133

3234
@property
@@ -37,21 +39,43 @@ def created_time(self) -> int:
3739
def user(self) -> "User":
3840
return self._user
3941

42+
@property
43+
def kind(self) -> str:
44+
return self._kind
45+
4046

4147
class AccessEvent(Event):
4248
def __init__(
4349
self,
4450
timestamp: int,
4551
user: "User",
4652
key: str,
47-
value: str,
53+
value: object,
4854
version: int,
49-
index: int):
50-
super().__init__(timestamp, user)
55+
variation_index: int,
56+
rule_index: int,
57+
reason: str,
58+
track_access_events: bool):
59+
super().__init__("access", timestamp, user)
5160
self._key = key
5261
self._value = value
5362
self._version = version
54-
self._index = index
63+
self._variation_index = variation_index
64+
self._rule_index = rule_index
65+
self._reason = reason
66+
self._track_access_events = track_access_events
67+
68+
def to_dict(self) -> dict:
69+
values = super().to_dict()
70+
values.update({
71+
'key': self._key,
72+
'value': self._value,
73+
'version': self._version,
74+
'variationIndex': self._variation_index,
75+
'ruleIndex': self._rule_index,
76+
'reason': self._reason
77+
})
78+
return values
5579

5680
@property
5781
def key(self):
@@ -66,5 +90,44 @@ def version(self):
6690
return self._version
6791

6892
@property
69-
def index(self):
70-
return self._index
93+
def variation_index(self):
94+
return self._variation_index
95+
96+
@property
97+
def rule_index(self):
98+
return self._rule_index
99+
100+
@property
101+
def reason(self):
102+
return self._reason
103+
104+
@property
105+
def track_access_events(self):
106+
return self._track_access_events
107+
108+
109+
class CustomEvent(Event):
110+
def __init__(
111+
self,
112+
timestamp: int,
113+
user: "User",
114+
name: str,
115+
value: Optional[float] = None):
116+
super().__init__("custom", timestamp, user)
117+
self._name = name
118+
self._value = value
119+
120+
def to_dict(self) -> dict:
121+
values = super().to_dict()
122+
values['name'] = self._name
123+
if self._value is not None:
124+
values['value'] = self._value
125+
return values
126+
127+
@property
128+
def name(self):
129+
return self._name
130+
131+
@property
132+
def value(self):
133+
return self._value

0 commit comments

Comments
 (0)