22import json
33import logging
44import threading
5- import time
65import typing
76from dataclasses import dataclass
87from datetime import datetime
1817# Used to control how often we send data(in seconds)
1918ANALYTICS_TIMER : typing .Final [int ] = 10
2019
20+ FLAG_EXPOSURE_EVENT : typing .Final [str ] = "$flag_exposure"
21+
22+ DEFAULT_EVENT_API_URL : typing .Final [str ] = "https://events.api.flagsmith.com/"
23+
2124session = FuturesSession (max_workers = 4 )
2225
2326
@@ -72,92 +75,90 @@ def track_feature(self, feature_name: str) -> None:
7275
7376
7477@dataclass
75- class PipelineAnalyticsConfig :
76- analytics_server_url : str
78+ class EventProcessorConfig :
79+ events_api_url : str = DEFAULT_EVENT_API_URL
7780 max_buffer_items : int = 1000
7881 flush_interval_seconds : float = 10.0
7982
8083
81- class PipelineAnalyticsProcessor :
84+ class EventProcessor :
8285 """
83- Buffered analytics processor that sends per-evaluation and custom events
84- to the Flagsmith pipeline analytics endpoint in batches.
85-
86- Evaluation events are deduplicated within each flush window. Events are
87- flushed periodically via a background timer or when the buffer is full.
86+ Buffered event processor that batches custom events and POSTs them to the
87+ Flagsmith event endpoint. Flushes on a background timer or when the buffer
88+ fills.
8889 """
8990
9091 def __init__ (
9192 self ,
92- config : PipelineAnalyticsConfig ,
93+ config : EventProcessorConfig ,
9394 environment_key : str ,
9495 ) -> None :
95- url = config .analytics_server_url
96+ url = config .events_api_url
9697 if not url .endswith ("/" ):
9798 url = f"{ url } /"
98- self ._batch_endpoint = f"{ url } v1/analytics/batch "
99+ self ._batch_endpoint = f"{ url } v1/events "
99100 self ._environment_key = environment_key
100101 self ._max_buffer = config .max_buffer_items
101102 self ._flush_interval_seconds = config .flush_interval_seconds
102103
103104 self ._buffer : typing .List [typing .Dict [str , typing .Any ]] = []
104- self ._dedup_keys : typing .Dict [str , str ] = {}
105105 self ._lock = threading .Lock ()
106106 self ._timer : typing .Optional [threading .Timer ] = None
107107
108- def record_evaluation_event (
108+ def track_event (
109109 self ,
110- flag_key : str ,
111- enabled : bool ,
112- value : typing .Any ,
113- identity_identifier : typing .Optional [str ] = None ,
110+ event : str ,
111+ identifier : typing .Optional [str ] = None ,
112+ value : typing .Optional [typing .Union [str , int , float , bool ]] = None ,
114113 traits : typing .Optional [typing .Dict [str , typing .Any ]] = None ,
114+ metadata : typing .Optional [typing .Dict [str , typing .Any ]] = None ,
115115 ) -> None :
116- fingerprint = f"{ identity_identifier or 'none' } |{ enabled } |{ value } "
117- should_flush = False
118-
119- with self ._lock :
120- if self ._dedup_keys .get (flag_key ) == fingerprint :
121- return
122- self ._dedup_keys [flag_key ] = fingerprint
123- self ._buffer .append (
124- {
125- "event_id" : flag_key ,
126- "event_type" : "flag_evaluation" ,
127- "evaluated_at" : int (time .time () * 1000 ),
128- "identity_identifier" : identity_identifier ,
129- "enabled" : enabled ,
130- "value" : value ,
131- "traits" : dict (traits ) if traits else None ,
132- "metadata" : {"sdk_version" : __version__ },
133- }
134- )
135- if len (self ._buffer ) >= self ._max_buffer :
136- should_flush = True
137-
138- if should_flush :
139- self .flush ()
116+ self ._buffer_event (
117+ event = event ,
118+ feature_name = None ,
119+ identifier = identifier ,
120+ value = value ,
121+ traits = traits ,
122+ metadata = metadata ,
123+ )
140124
141- def record_custom_event (
125+ def track_exposure_event (
142126 self ,
143- event_name : str ,
144- identity_identifier : typing .Optional [str ] = None ,
127+ feature_name : str ,
128+ identifier : typing .Optional [str ] = None ,
129+ value : typing .Optional [typing .Union [str , int , float , bool ]] = None ,
145130 traits : typing .Optional [typing .Dict [str , typing .Any ]] = None ,
146131 metadata : typing .Optional [typing .Dict [str , typing .Any ]] = None ,
147132 ) -> None :
148- should_flush = False
133+ self ._buffer_event (
134+ event = FLAG_EXPOSURE_EVENT ,
135+ feature_name = feature_name ,
136+ identifier = identifier ,
137+ value = value ,
138+ traits = traits ,
139+ metadata = metadata ,
140+ )
149141
142+ def _buffer_event (
143+ self ,
144+ event : str ,
145+ feature_name : typing .Optional [str ],
146+ identifier : typing .Optional [str ],
147+ value : typing .Optional [typing .Union [str , int , float , bool ]],
148+ traits : typing .Optional [typing .Dict [str , typing .Any ]],
149+ metadata : typing .Optional [typing .Dict [str , typing .Any ]],
150+ ) -> None :
151+ should_flush = False
150152 with self ._lock :
151153 self ._buffer .append (
152154 {
153- "event_id" : event_name ,
154- "event_type" : "custom_event" ,
155- "evaluated_at" : int (time .time () * 1000 ),
156- "identity_identifier" : identity_identifier ,
157- "enabled" : None ,
158- "value" : None ,
155+ "event" : event ,
156+ "feature_name" : feature_name ,
157+ "identifier" : identifier ,
158+ "value" : str (value ) if value is not None else None ,
159159 "traits" : dict (traits ) if traits else None ,
160160 "metadata" : {** (metadata or {}), "sdk_version" : __version__ },
161+ "timestamp" : int (datetime .now ().timestamp () * 1000 ),
161162 }
162163 )
163164 if len (self ._buffer ) >= self ._max_buffer :
@@ -172,11 +173,8 @@ def flush(self) -> None:
172173 return
173174 events = self ._buffer
174175 self ._buffer = []
175- self ._dedup_keys .clear ()
176176
177- payload = json .dumps (
178- {"events" : events , "environment_key" : self ._environment_key }
179- )
177+ payload = json .dumps ({"events" : events })
180178 try :
181179 future = session .post (
182180 self ._batch_endpoint ,
0 commit comments