1+ import 'package:on_time_front/domain/entities/schedule_preparation_mode.dart' ;
2+
13class ProductUsageEvent {
2- const ProductUsageEvent ({
3- required this .name,
4- required this .workflow,
5- required this .result,
6- this .parameters = const {},
7- });
4+ ProductUsageEvent ._({
5+ required ProductUsageEventDefinition definition,
6+ required ProductUsageResult eventResult,
7+ Map <String , Object > parameters = const {},
8+ }) : name = definition.name,
9+ workflow = definition.workflow.wireValue,
10+ result = eventResult.wireValue,
11+ schemaVersion = definition.schemaVersion,
12+ parameters = ProductUsageEventCatalog .validateParameters (
13+ definition,
14+ parameters,
15+ );
16+
17+ factory ProductUsageEvent .fromCatalog ({
18+ required String name,
19+ required ProductUsageResult result,
20+ Map <String , Object > parameters = const {},
21+ }) {
22+ final definition = ProductUsageEventCatalog .definitionFor (name);
23+ return ProductUsageEvent ._(
24+ definition: definition,
25+ eventResult: result,
26+ parameters: parameters,
27+ );
28+ }
29+
30+ factory ProductUsageEvent .scheduleCreated ({
31+ required SchedulePreparationMode ? preparationMode,
32+ required int preparationStepCount,
33+ required int minutesUntilSchedule,
34+ }) {
35+ return ProductUsageEvent ._(
36+ definition: ProductUsageEventCatalog .scheduleCreated,
37+ eventResult: ProductUsageResult .success,
38+ parameters: {
39+ 'preparation_mode' : _preparationModeWireValue (preparationMode),
40+ 'preparation_step_count' : preparationStepCount,
41+ 'minutes_until_schedule' : minutesUntilSchedule,
42+ },
43+ );
44+ }
845
946 final String name;
1047 final String workflow;
1148 final String result;
49+ final int schemaVersion;
1250 final Map <String , Object > parameters;
1351
1452 Map <String , Object > toAnalyticsParameters ({
1553 required String platform,
1654 required String appVersion,
1755 }) {
1856 return {
19- 'schema_version' : 1 ,
57+ 'schema_version' : schemaVersion ,
2058 'workflow' : workflow,
2159 'result' : result,
2260 'platform' : platform,
@@ -25,3 +63,286 @@ class ProductUsageEvent {
2563 };
2664 }
2765}
66+
67+ enum ProductUsageResult {
68+ success ('success' ),
69+ failure ('failure' ),
70+ allowed ('allowed' ),
71+ denied ('denied' ),
72+ disabled ('disabled' );
73+
74+ const ProductUsageResult (this .wireValue);
75+
76+ final String wireValue;
77+ }
78+
79+ enum ProductUsageWorkflow {
80+ analytics ('analytics' ),
81+ onboarding ('onboarding' ),
82+ authentication ('authentication' ),
83+ schedule ('schedule' ),
84+ notification ('notification' ),
85+ alarm ('alarm' );
86+
87+ const ProductUsageWorkflow (this .wireValue);
88+
89+ final String wireValue;
90+ }
91+
92+ enum ProductUsageEventParameterKey {
93+ enabled ('enabled' ),
94+ source ('source' ),
95+ preparationStepCount ('preparation_step_count' ),
96+ spareTimeMinutes ('spare_time_minutes' ),
97+ authProvider ('auth_provider' ),
98+ preparationMode ('preparation_mode' ),
99+ minutesUntilSchedule ('minutes_until_schedule' ),
100+ preparationChanged ('preparation_changed' ),
101+ permissionResult ('permission_result' ),
102+ launchAction ('launch_action' ),
103+ provider ('provider' ),
104+ errorCode ('error_code' ),
105+ latenessBucket ('lateness_bucket' ),
106+ startedEarly ('started_early' );
107+
108+ const ProductUsageEventParameterKey (this .wireValue);
109+
110+ final String wireValue;
111+
112+ static ProductUsageEventParameterKey ? fromWireValue (String wireValue) {
113+ for (final key in ProductUsageEventParameterKey .values) {
114+ if (key.wireValue == wireValue) return key;
115+ }
116+ return null ;
117+ }
118+ }
119+
120+ class ProductUsageEventDefinition {
121+ const ProductUsageEventDefinition ({
122+ required this .name,
123+ required this .workflow,
124+ required this .allowedParameters,
125+ this .schemaVersion = 1 ,
126+ });
127+
128+ final String name;
129+ final ProductUsageWorkflow workflow;
130+ final Set <ProductUsageEventParameterKey > allowedParameters;
131+ final int schemaVersion;
132+
133+ Set <String > get allowedParameterNames => Set .unmodifiable (
134+ allowedParameters.map ((parameter) => parameter.wireValue),
135+ );
136+ }
137+
138+ class ProductUsageEventCatalog {
139+ const ProductUsageEventCatalog ._();
140+
141+ static const analyticsPreferenceChanged = ProductUsageEventDefinition (
142+ name: 'analytics_preference_changed' ,
143+ workflow: ProductUsageWorkflow .analytics,
144+ allowedParameters: {
145+ ProductUsageEventParameterKey .enabled,
146+ ProductUsageEventParameterKey .source,
147+ },
148+ );
149+
150+ static const onboardingCompleted = ProductUsageEventDefinition (
151+ name: 'onboarding_completed' ,
152+ workflow: ProductUsageWorkflow .onboarding,
153+ allowedParameters: {
154+ ProductUsageEventParameterKey .preparationStepCount,
155+ ProductUsageEventParameterKey .spareTimeMinutes,
156+ },
157+ );
158+
159+ static const signUpCompleted = ProductUsageEventDefinition (
160+ name: 'sign_up_completed' ,
161+ workflow: ProductUsageWorkflow .authentication,
162+ allowedParameters: {ProductUsageEventParameterKey .authProvider},
163+ );
164+
165+ static const loginCompleted = ProductUsageEventDefinition (
166+ name: 'login_completed' ,
167+ workflow: ProductUsageWorkflow .authentication,
168+ allowedParameters: {ProductUsageEventParameterKey .authProvider},
169+ );
170+
171+ static const scheduleCreateStarted = ProductUsageEventDefinition (
172+ name: 'schedule_create_started' ,
173+ workflow: ProductUsageWorkflow .schedule,
174+ allowedParameters: {ProductUsageEventParameterKey .source},
175+ );
176+
177+ static const scheduleCreated = ProductUsageEventDefinition (
178+ name: 'schedule_created' ,
179+ workflow: ProductUsageWorkflow .schedule,
180+ allowedParameters: {
181+ ProductUsageEventParameterKey .preparationMode,
182+ ProductUsageEventParameterKey .preparationStepCount,
183+ ProductUsageEventParameterKey .minutesUntilSchedule,
184+ },
185+ );
186+
187+ static const scheduleUpdated = ProductUsageEventDefinition (
188+ name: 'schedule_updated' ,
189+ workflow: ProductUsageWorkflow .schedule,
190+ allowedParameters: {
191+ ProductUsageEventParameterKey .preparationChanged,
192+ ProductUsageEventParameterKey .minutesUntilSchedule,
193+ },
194+ );
195+
196+ static const scheduleDeleted = ProductUsageEventDefinition (
197+ name: 'schedule_deleted' ,
198+ workflow: ProductUsageWorkflow .schedule,
199+ allowedParameters: {ProductUsageEventParameterKey .minutesUntilSchedule},
200+ );
201+
202+ static const notificationPermissionResult = ProductUsageEventDefinition (
203+ name: 'notification_permission_result' ,
204+ workflow: ProductUsageWorkflow .notification,
205+ allowedParameters: {
206+ ProductUsageEventParameterKey .permissionResult,
207+ ProductUsageEventParameterKey .source,
208+ },
209+ );
210+
211+ static const alarmOpened = ProductUsageEventDefinition (
212+ name: 'alarm_opened' ,
213+ workflow: ProductUsageWorkflow .alarm,
214+ allowedParameters: {
215+ ProductUsageEventParameterKey .launchAction,
216+ ProductUsageEventParameterKey .provider,
217+ },
218+ );
219+
220+ static const alarmFailed = ProductUsageEventDefinition (
221+ name: 'alarm_failed' ,
222+ workflow: ProductUsageWorkflow .alarm,
223+ allowedParameters: {
224+ ProductUsageEventParameterKey .errorCode,
225+ ProductUsageEventParameterKey .provider,
226+ },
227+ );
228+
229+ static const scheduleFinished = ProductUsageEventDefinition (
230+ name: 'schedule_finished' ,
231+ workflow: ProductUsageWorkflow .schedule,
232+ allowedParameters: {
233+ ProductUsageEventParameterKey .latenessBucket,
234+ ProductUsageEventParameterKey .preparationStepCount,
235+ ProductUsageEventParameterKey .startedEarly,
236+ },
237+ );
238+
239+ static const firstReleaseEvents = < ProductUsageEventDefinition > [
240+ analyticsPreferenceChanged,
241+ onboardingCompleted,
242+ signUpCompleted,
243+ loginCompleted,
244+ scheduleCreateStarted,
245+ scheduleCreated,
246+ scheduleUpdated,
247+ scheduleDeleted,
248+ notificationPermissionResult,
249+ alarmOpened,
250+ alarmFailed,
251+ scheduleFinished,
252+ ];
253+
254+ static final Map <String , ProductUsageEventDefinition > _definitionsByName = {
255+ for (final definition in firstReleaseEvents) definition.name: definition,
256+ };
257+
258+ static const _forbiddenParameterNames = < String > {
259+ 'email' ,
260+ 'display_name' ,
261+ 'oauth_identifier' ,
262+ 'fcm_token' ,
263+ 'access_token' ,
264+ 'refresh_token' ,
265+ 'schedule_name' ,
266+ 'schedule_note' ,
267+ 'place_name' ,
268+ 'preparation_step_name' ,
269+ 'exception' ,
270+ 'stack_trace' ,
271+ 'request_body' ,
272+ 'response_body' ,
273+ 'location' ,
274+ 'latitude' ,
275+ 'longitude' ,
276+ };
277+
278+ static ProductUsageEventDefinition definitionFor (String name) {
279+ final definition = _definitionsByName[name];
280+ if (definition == null ) {
281+ throw ProductUsageEventCatalogException (
282+ 'Unknown Product Usage Event: $name ' ,
283+ );
284+ }
285+ return definition;
286+ }
287+
288+ static Map <String , Object > validateParameters (
289+ ProductUsageEventDefinition definition,
290+ Map <String , Object > parameters,
291+ ) {
292+ final validatedParameters = < String , Object > {};
293+ for (final entry in parameters.entries) {
294+ final key = entry.key;
295+ final value = entry.value;
296+ if (_forbiddenParameterNames.contains (key)) {
297+ throw ProductUsageEventCatalogException (
298+ 'Forbidden Analytics Event Parameter: $key ' ,
299+ );
300+ }
301+
302+ final parameterKey = ProductUsageEventParameterKey .fromWireValue (key);
303+ if (parameterKey == null ||
304+ ! definition.allowedParameters.contains (parameterKey)) {
305+ throw ProductUsageEventCatalogException (
306+ 'Parameter $key is not allowed for ${definition .name }' ,
307+ );
308+ }
309+
310+ _validateParameterValue (key, value);
311+ validatedParameters[key] = value;
312+ }
313+ return Map .unmodifiable (validatedParameters);
314+ }
315+
316+ static void _validateParameterValue (String key, Object value) {
317+ if (value is Map || value is Iterable ) {
318+ throw ProductUsageEventCatalogException (
319+ 'Parameter $key must be a scalar analytics value' ,
320+ );
321+ }
322+ if (value is String || value is num || value is bool ) return ;
323+ throw ProductUsageEventCatalogException (
324+ 'Parameter $key has unsupported value type ${value .runtimeType }' ,
325+ );
326+ }
327+ }
328+
329+ class ProductUsageEventCatalogException implements Exception {
330+ const ProductUsageEventCatalogException (this .message);
331+
332+ final String message;
333+
334+ @override
335+ String toString () => message;
336+ }
337+
338+ String _preparationModeWireValue (SchedulePreparationMode ? mode) {
339+ switch (mode) {
340+ case SchedulePreparationMode .template:
341+ return 'template' ;
342+ case SchedulePreparationMode .custom:
343+ return 'custom' ;
344+ case SchedulePreparationMode .defaultPreparation:
345+ case null :
346+ return 'default' ;
347+ }
348+ }
0 commit comments