-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Automatic assignment tracking (#25)
- Loading branch information
Showing
19 changed files
with
382 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
amplitude_analytics~=1.1.1 |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
from .assignment import Assignment, DAY_MILLIS | ||
from .assignment_filter import AssignmentFilter | ||
from .assignment_service import AssignmentService, to_event | ||
from .assignment_config import AssignmentConfig |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import time | ||
from typing import Dict | ||
|
||
from ..flagresult import FlagResult | ||
from ..user import User | ||
|
||
DAY_MILLIS = 24 * 60 * 60 * 1000 | ||
|
||
|
||
class Assignment: | ||
|
||
def __init__(self, user: User, results: Dict[str, FlagResult]): | ||
self.user = user | ||
self.results = results | ||
self.timestamp = time.time() | ||
|
||
def canonicalize(self) -> str: | ||
user = self.user.user_id.strip() if self.user.user_id else 'undefined' | ||
device = self.user.device_id.strip() if self.user.device_id else 'undefined' | ||
canonical = user + ' ' + device + ' ' | ||
for key in sorted(self.results): | ||
value = self.results[key].value.strip() if self.results[key] else 'undefined' | ||
canonical += key.strip() + ' ' + value + ' ' | ||
return canonical |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import amplitude | ||
|
||
|
||
class AssignmentConfig(amplitude.Config): | ||
def __init__(self, api_key: str, cache_capacity: int = 65536, **kw): | ||
self.api_key = api_key | ||
self.cache_capacity = cache_capacity | ||
super(AssignmentConfig, self).__init__(**kw) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import time | ||
|
||
from .assignment import Assignment | ||
from .assignment import DAY_MILLIS | ||
from ..util.cache import Cache | ||
|
||
|
||
class AssignmentFilter: | ||
def __init__(self, size: int, ttl_millis: int = DAY_MILLIS): | ||
self.cache = Cache(size, ttl_millis) | ||
|
||
def should_track(self, assignment: Assignment) -> bool: | ||
canonical_assignment = assignment.canonicalize() | ||
track = self.cache.get(canonical_assignment) is None | ||
if track: | ||
self.cache.put(canonical_assignment, object()) | ||
return track |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
from amplitude import Amplitude, BaseEvent | ||
from ..assignment.assignment import Assignment | ||
from ..assignment.assignment import DAY_MILLIS | ||
from ..assignment.assignment_filter import AssignmentFilter | ||
|
||
FLAG_TYPE_MUTUAL_EXCLUSION_GROUP = "mutual-exclusion-group" | ||
FLAG_TYPE_HOLDOUT_GROUP = "holdout-group" | ||
|
||
|
||
def to_event(assignment: Assignment) -> BaseEvent: | ||
event = BaseEvent(event_type='[Experiment] Assignment', user_id=assignment.user.user_id, | ||
device_id=assignment.user.device_id, event_properties={}, user_properties={}) | ||
for key in sorted(assignment.results): | ||
event.event_properties[key + '.variant'] = assignment.results[key].value | ||
|
||
set_props = {} | ||
unset_props = {} | ||
|
||
for key in sorted(assignment.results): | ||
if assignment.results[key].type == FLAG_TYPE_MUTUAL_EXCLUSION_GROUP: | ||
continue | ||
elif assignment.results[key].is_default_variant: | ||
unset_props[f'[Experiment] {key}'] = '-' | ||
else: | ||
set_props[f'[Experiment] {key}'] = assignment.results[key].value | ||
|
||
event.user_properties['$set'] = set_props | ||
event.user_properties['$unset'] = unset_props | ||
|
||
event.insert_id = f'{event.user_id} {event.device_id} {hash(assignment.canonicalize())} {assignment.timestamp / DAY_MILLIS}' | ||
|
||
return event | ||
|
||
|
||
class AssignmentService: | ||
def __init__(self, amplitude: Amplitude, assignment_filter: AssignmentFilter): | ||
self.amplitude = amplitude | ||
self.assignmentFilter = assignment_filter | ||
|
||
def track(self, assignment: Assignment): | ||
if self.assignmentFilter.should_track(assignment): | ||
self.amplitude.track(to_event(assignment)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
class FlagResult: | ||
def __init__(self, value: str, is_default_variant: bool, payload: str = None, exp_key: str = None, | ||
deployed: bool = None, type: str = None): | ||
self.value = value | ||
self.payload = payload | ||
self.is_default_variant = is_default_variant | ||
self.exp_key = exp_key | ||
self.deployed = deployed | ||
self.type = type |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .cache import Cache |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import time | ||
|
||
class Cache: | ||
class Node: | ||
def __init__(self, key, value): | ||
self.key = key | ||
self.value = value | ||
self.prev = None | ||
self.next = None | ||
self.last_accessed_time = time.time() | ||
|
||
def __init__(self, capacity, ttl_millis): | ||
self.capacity = capacity | ||
self.ttl_millis = ttl_millis | ||
self.cache = {} | ||
self.head = self.Node(None, None) | ||
self.tail = self.Node(None, None) | ||
self.head.next = self.tail | ||
self.tail.prev = self.head | ||
|
||
def _add_node(self, node): | ||
node.prev = self.head | ||
node.next = self.head.next | ||
self.head.next.prev = node | ||
self.head.next = node | ||
|
||
def _remove_node(self, node): | ||
prev = node.prev | ||
next_node = node.next | ||
prev.next = next_node | ||
next_node.prev = prev | ||
|
||
def _move_to_head(self, node): | ||
self._remove_node(node) | ||
self._add_node(node) | ||
|
||
def get(self, key): | ||
if key in self.cache: | ||
node = self.cache[key] | ||
current_time = time.time() | ||
if (current_time - node.last_accessed_time) * 1000 <= self.ttl_millis: | ||
node.last_accessed_time = current_time # Update last accessed time | ||
self._move_to_head(node) | ||
return node.value | ||
else: | ||
# Node has expired, remove it from the cache | ||
self._remove_node(node) | ||
del self.cache[key] | ||
return None | ||
|
||
def put(self, key, value): | ||
if key in self.cache: | ||
node = self.cache[key] | ||
node.value = value | ||
node.last_accessed_time = time.time() # Update last accessed time | ||
self._move_to_head(node) | ||
else: | ||
if len(self.cache) >= self.capacity: | ||
# Evict the least recently used node (tail's prev) | ||
tail_prev = self.tail.prev | ||
self._remove_node(tail_prev) | ||
del self.cache[tail_prev.key] | ||
|
||
new_node = self.Node(key, value) | ||
self._add_node(new_node) | ||
self.cache[key] = new_node | ||
|
Empty file.
Oops, something went wrong.