@eventvisor/sdk 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +9 -0
  3. package/dist/attributesManager.d.ts +36 -0
  4. package/dist/bucketer.d.ts +30 -0
  5. package/dist/compareVersions.d.ts +4 -0
  6. package/dist/conditions.d.ts +20 -0
  7. package/dist/datafileReader.d.ts +29 -0
  8. package/dist/effectsManager.d.ts +33 -0
  9. package/dist/emitter.d.ts +11 -0
  10. package/dist/index.d.ts +12 -0
  11. package/dist/index.js +2 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/index.mjs +2 -0
  14. package/dist/index.mjs.gz +0 -0
  15. package/dist/index.mjs.map +1 -0
  16. package/dist/instance.d.ts +67 -0
  17. package/dist/logger.d.ts +26 -0
  18. package/dist/modulesManager.d.ts +67 -0
  19. package/dist/murmurhash.d.ts +1 -0
  20. package/dist/persister.d.ts +40 -0
  21. package/dist/sourceResolver.d.ts +31 -0
  22. package/dist/transformer.d.ts +21 -0
  23. package/dist/validator.d.ts +28 -0
  24. package/jest.config.js +6 -0
  25. package/lib/attributesManager.d.ts +36 -0
  26. package/lib/bucketer.d.ts +30 -0
  27. package/lib/compareVersions.d.ts +4 -0
  28. package/lib/conditions.d.ts +20 -0
  29. package/lib/datafileReader.d.ts +29 -0
  30. package/lib/effectsManager.d.ts +33 -0
  31. package/lib/emitter.d.ts +11 -0
  32. package/lib/index.d.ts +12 -0
  33. package/lib/instance.d.ts +67 -0
  34. package/lib/logger.d.ts +26 -0
  35. package/lib/modulesManager.d.ts +67 -0
  36. package/lib/murmurhash.d.ts +1 -0
  37. package/lib/persister.d.ts +40 -0
  38. package/lib/sourceResolver.d.ts +31 -0
  39. package/lib/transformer.d.ts +21 -0
  40. package/lib/validator.d.ts +28 -0
  41. package/package.json +45 -0
  42. package/src/attributesManager.ts +181 -0
  43. package/src/bucketer.spec.ts +156 -0
  44. package/src/bucketer.ts +152 -0
  45. package/src/compareVersions.ts +93 -0
  46. package/src/conditions.ts +224 -0
  47. package/src/datafileReader.ts +133 -0
  48. package/src/effectsManager.ts +214 -0
  49. package/src/emitter.ts +64 -0
  50. package/src/index.spec.ts +5 -0
  51. package/src/index.ts +14 -0
  52. package/src/instance.spec.ts +184 -0
  53. package/src/instance.ts +608 -0
  54. package/src/logger.ts +90 -0
  55. package/src/modulesManager.ts +276 -0
  56. package/src/murmurhash.ts +71 -0
  57. package/src/persister.ts +162 -0
  58. package/src/sourceResolver.spec.ts +253 -0
  59. package/src/sourceResolver.ts +213 -0
  60. package/src/transformer.ts +316 -0
  61. package/src/transformer_static.spec.ts +377 -0
  62. package/src/transformer_types.spec.ts +820 -0
  63. package/src/validator.spec.ts +579 -0
  64. package/src/validator.ts +366 -0
  65. package/tsconfig.cjs.json +8 -0
  66. package/tsconfig.esm.json +8 -0
  67. package/webpack.config.js +80 -0
@@ -0,0 +1,608 @@
1
+ import {
2
+ AttributeName,
3
+ DatafileContent,
4
+ EventName,
5
+ EffectName,
6
+ Value,
7
+ Action,
8
+ } from "@eventvisor/types";
9
+
10
+ import { DatafileReader, emptyDatafile } from "./datafileReader";
11
+ import { createLogger, Logger, LogLevel } from "./logger";
12
+ import { Emitter, EmitType, EventCallback } from "./emitter";
13
+ import { AttributesManager } from "./attributesManager";
14
+ import { Module, ModuleName, ModulesManager } from "./modulesManager";
15
+ import { SourceResolver } from "./sourceResolver";
16
+ import { ConditionsChecker } from "./conditions";
17
+ import { Bucketer } from "./bucketer";
18
+ import { Transformer } from "./transformer";
19
+ import { Validator } from "./validator";
20
+ import { EffectsManager } from "./effectsManager";
21
+
22
+ export interface InstanceOptions {
23
+ datafile?: DatafileContent;
24
+ logLevel?: LogLevel;
25
+ logger?: Logger;
26
+ modules?: Module[];
27
+
28
+ // @TODO
29
+ // initialAttributes?: Record<AttributeName, Value>;
30
+ }
31
+
32
+ export class Eventvisor {
33
+ private datafileReader: DatafileReader;
34
+ private logger: Logger;
35
+ private emitter: Emitter;
36
+ private attributesManager: AttributesManager;
37
+ private modulesManager: ModulesManager;
38
+ private effectsManager: EffectsManager;
39
+ private sourceResolver: SourceResolver;
40
+ private conditionsChecker: ConditionsChecker;
41
+ private transformer: Transformer;
42
+ private bucketer: Bucketer;
43
+ private validator: Validator;
44
+
45
+ private ready: boolean = false;
46
+ private queue: Action[] = [];
47
+ private queueProcessing: boolean = false;
48
+
49
+ constructor(options: InstanceOptions = {}) {
50
+ /**
51
+ * Core instances without interdependencies
52
+ *
53
+ * @TODO: sort out this dependency mess!!
54
+ */
55
+ this.logger =
56
+ options.logger ||
57
+ createLogger({
58
+ level: options.logLevel || Logger.defaultLevel,
59
+ });
60
+
61
+ this.datafileReader = new DatafileReader({
62
+ datafile: options.datafile || emptyDatafile,
63
+ logger: this.logger,
64
+ });
65
+
66
+ this.emitter = new Emitter();
67
+
68
+ /**
69
+ * Instances with interdependencies
70
+ */
71
+ this.modulesManager = new ModulesManager({
72
+ logger: this.logger,
73
+ getDatafileReader: () => this.datafileReader,
74
+ getSourceResolver: () => this.sourceResolver,
75
+ });
76
+
77
+ this.validator = new Validator({
78
+ logger: this.logger,
79
+ getSourceResolver: () => this.sourceResolver,
80
+ });
81
+
82
+ this.attributesManager = new AttributesManager({
83
+ logger: this.logger,
84
+ emitter: this.emitter,
85
+ validator: this.validator,
86
+ getDatafileReader: () => this.datafileReader,
87
+ getTransformer: () => this.transformer,
88
+ getConditionsChecker: () => this.conditionsChecker,
89
+ modulesManager: this.modulesManager,
90
+ });
91
+
92
+ this.effectsManager = new EffectsManager({
93
+ logger: this.logger,
94
+ getDatafileReader: () => this.datafileReader,
95
+ getTransformer: () => this.transformer,
96
+ getConditionsChecker: () => this.conditionsChecker,
97
+ modulesManager: this.modulesManager,
98
+ });
99
+
100
+ this.sourceResolver = new SourceResolver({
101
+ logger: this.logger,
102
+ modulesManager: this.modulesManager,
103
+ attributesManager: this.attributesManager,
104
+ effectsManager: this.effectsManager,
105
+ });
106
+
107
+ this.conditionsChecker = new ConditionsChecker({
108
+ logger: this.logger,
109
+ getRegex: (regexString, regexFlags) => this.datafileReader.getRegex(regexString, regexFlags),
110
+ sourceResolver: this.sourceResolver,
111
+ });
112
+
113
+ this.transformer = new Transformer({
114
+ logger: this.logger,
115
+ conditionsChecker: this.conditionsChecker,
116
+ sourceResolver: this.sourceResolver,
117
+ });
118
+
119
+ this.bucketer = new Bucketer({
120
+ logger: this.logger,
121
+ sourceResolver: this.sourceResolver,
122
+ conditionsChecker: this.conditionsChecker,
123
+ transformer: this.transformer,
124
+ });
125
+
126
+ /**
127
+ * Ready
128
+ */
129
+ if (options.modules) {
130
+ for (const module of options.modules) {
131
+ this.modulesManager.registerModule(module);
132
+ }
133
+ }
134
+
135
+ Promise.all([this.effectsManager.initialize(), this.attributesManager.initialize()])
136
+ .then(() => {
137
+ this.ready = true;
138
+ this.emitter.trigger("ready");
139
+ this.logger.debug("Eventvisor SDK is ready");
140
+ })
141
+ .catch((error) => {
142
+ this.logger.error("initialization failed", error);
143
+ });
144
+
145
+ this.logger.info("Eventvisor SDK initialized");
146
+ }
147
+
148
+ isReady() {
149
+ return this.ready;
150
+ }
151
+
152
+ async onReady(): Promise<void> {
153
+ if (this.ready) {
154
+ return;
155
+ }
156
+
157
+ return new Promise((resolve) => {
158
+ const unsubscribe = this.emitter.on("ready", () => {
159
+ unsubscribe();
160
+ resolve();
161
+ });
162
+ });
163
+ }
164
+
165
+ getRevision() {
166
+ return this.datafileReader.getRevision();
167
+ }
168
+
169
+ setLogLevel(level: LogLevel) {
170
+ return this.logger.setLevel(level);
171
+ }
172
+
173
+ setDatafile(datafile: DatafileContent) {
174
+ try {
175
+ const newDatafileReader = new DatafileReader({
176
+ datafile,
177
+ logger: this.logger,
178
+ });
179
+
180
+ this.datafileReader = newDatafileReader;
181
+
182
+ this.effectsManager.refresh();
183
+
184
+ this.emitter.trigger("datafile_set");
185
+ } catch (error) {
186
+ this.logger.error("Error setting datafile", {
187
+ error,
188
+ });
189
+ }
190
+ }
191
+
192
+ on(emitType: EmitType, callback: EventCallback) {
193
+ return this.emitter.on(emitType, callback);
194
+ }
195
+
196
+ /**
197
+ * Queue
198
+ */
199
+ private addToQueue(action: Action) {
200
+ this.queue.push(action);
201
+ }
202
+
203
+ // @TODO: make it better
204
+ private async processQueue() {
205
+ if (this.queue.length === 0) {
206
+ return;
207
+ }
208
+
209
+ if (this.queueProcessing) {
210
+ return;
211
+ }
212
+
213
+ this.queueProcessing = true;
214
+
215
+ const action = this.queue.shift();
216
+
217
+ if (!action) {
218
+ this.queueProcessing = false;
219
+
220
+ return;
221
+ }
222
+
223
+ try {
224
+ if (action.type === "track") {
225
+ await this.trackAsync(action.name, action.value);
226
+ } else if (action.type === "setAttribute") {
227
+ await this.setAttributeAsync(action.name, action.value);
228
+ } else if (action.type === "removeAttribute") {
229
+ await this.removeAttributeAsync(action.name);
230
+ } else if (action.type === "removeAttribute") {
231
+ await this.removeAttributeAsync(action.name);
232
+ }
233
+ } catch (error) {
234
+ this.logger.error(`Error processing queue`, {
235
+ error,
236
+ action,
237
+ });
238
+ }
239
+
240
+ this.queueProcessing = false;
241
+
242
+ await this.processQueue();
243
+ }
244
+
245
+ /**
246
+ * Attribute
247
+ */
248
+ async setAttributeAsync(attributeName: AttributeName, value: Value) {
249
+ const result = await this.attributesManager.setAttribute(attributeName, value);
250
+
251
+ /**
252
+ * Effects
253
+ */
254
+ await this.effectsManager.dispatch({
255
+ eventType: "attribute_set",
256
+ name: attributeName,
257
+ value: result,
258
+ });
259
+
260
+ return result;
261
+ }
262
+
263
+ setAttribute(attributeName: AttributeName, value: Value) {
264
+ this.addToQueue({
265
+ type: "setAttribute",
266
+ name: attributeName,
267
+ value,
268
+ });
269
+
270
+ this.processQueue();
271
+ }
272
+
273
+ getAttributeValue(attributeName: AttributeName) {
274
+ return this.attributesManager.getAttributeValue(attributeName);
275
+ }
276
+
277
+ getAttributes() {
278
+ return this.attributesManager.getAttributesMap();
279
+ }
280
+
281
+ isAttributeSet(attributeName: AttributeName) {
282
+ return this.attributesManager.isAttributeSet(attributeName);
283
+ }
284
+
285
+ removeAttributeAsync(attributeName: AttributeName) {
286
+ return this.attributesManager.removeAttribute(attributeName);
287
+ }
288
+
289
+ removeAttribute(attributeName: AttributeName) {
290
+ this.addToQueue({
291
+ type: "removeAttribute",
292
+ name: attributeName,
293
+ });
294
+
295
+ this.processQueue();
296
+ }
297
+
298
+ /**
299
+ * Modules
300
+ */
301
+ registerModule(module: Module) {
302
+ return this.modulesManager.registerModule(module);
303
+ }
304
+
305
+ removeModule(moduleName: ModuleName) {
306
+ return this.modulesManager.removeModule(moduleName);
307
+ }
308
+
309
+ /**
310
+ * Event
311
+ */
312
+ async trackAsync(eventName: EventName, value: Value): Promise<Value | null> {
313
+ /**
314
+ * Find
315
+ */
316
+ const eventSchema = this.datafileReader.getEvent(eventName);
317
+
318
+ if (!eventSchema) {
319
+ this.logger.error(`Event schema not found in datafile`, { eventName });
320
+
321
+ return null; // @TODO: allow to continue based on SDK instance options later
322
+ }
323
+
324
+ const eventLevel = eventSchema.level || "info";
325
+
326
+ /**
327
+ * Deprecated
328
+ */
329
+ if (eventSchema.deprecated) {
330
+ this.logger.warn(`Event is deprecated`, { eventName });
331
+ }
332
+
333
+ /**
334
+ * Validate
335
+ */
336
+ const validationResult = await this.validator.validate(eventSchema, value);
337
+
338
+ if (!validationResult.valid) {
339
+ this.logger.warn(`Event validation failed`, {
340
+ eventName,
341
+ errors: validationResult.errors,
342
+ });
343
+
344
+ return null; // @TODO: allow to continue based on schema later
345
+ }
346
+
347
+ const validatedValue = validationResult.value;
348
+
349
+ /**
350
+ * Conditions
351
+ */
352
+ if (eventSchema.conditions) {
353
+ const isMatched = await this.conditionsChecker.allAreMatched(eventSchema.conditions, {
354
+ // @TODO: rename to eventPayload to be explicit?
355
+ eventName,
356
+ eventLevel,
357
+ payload: validatedValue,
358
+ });
359
+
360
+ if (!isMatched) {
361
+ this.logger.debug(`Event conditions not matched`, {
362
+ eventName,
363
+ conditions: eventSchema.conditions,
364
+ });
365
+
366
+ return null;
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Sample
372
+ */
373
+ if (eventSchema.sample) {
374
+ const sampleResult = await this.bucketer.isSampled(eventSchema.sample, {
375
+ eventName,
376
+ eventLevel,
377
+ payload: validatedValue,
378
+ });
379
+
380
+ if (!sampleResult.isSampled) {
381
+ this.logger.debug(`Event sample not matched`, {
382
+ eventName,
383
+ matchedSample: sampleResult.matchedSample,
384
+ bucketedNumber: sampleResult.bucketedNumber,
385
+ bucketKey: sampleResult.bucketKey,
386
+ });
387
+
388
+ return null;
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Transform
394
+ */
395
+ let transformedValue = validatedValue;
396
+
397
+ if (eventSchema.transforms) {
398
+ transformedValue = await this.transformer.applyAll(validatedValue, eventSchema.transforms, {
399
+ eventName,
400
+ eventLevel,
401
+ payload: validatedValue,
402
+ });
403
+ }
404
+
405
+ /**
406
+ * Effects
407
+ */
408
+ await this.effectsManager.dispatch({
409
+ eventType: "event_tracked",
410
+ name: eventName,
411
+ value: transformedValue,
412
+ });
413
+
414
+ /**
415
+ * Destinations
416
+ */
417
+ const destinationNames = this.datafileReader.getDestinationNames();
418
+
419
+ for (const destinationName of destinationNames) {
420
+ const destination = this.datafileReader.getDestination(destinationName);
421
+
422
+ if (!destination) {
423
+ continue;
424
+ }
425
+
426
+ const transportExists = this.modulesManager.transportExists(destination.transport);
427
+
428
+ if (!transportExists) {
429
+ this.logger.error(`Destination has no transport`, {
430
+ eventName,
431
+ destinationName,
432
+ });
433
+
434
+ continue;
435
+ }
436
+
437
+ let transportBody = transformedValue;
438
+
439
+ /**
440
+ * Event.destinations
441
+ */
442
+ if (
443
+ eventSchema.destinations &&
444
+ typeof eventSchema.destinations[destinationName] !== "undefined"
445
+ ) {
446
+ const destinationOverride = eventSchema.destinations[destinationName];
447
+
448
+ if (destinationOverride === false) {
449
+ this.logger.debug(`Event has destination disabled`, {
450
+ eventName,
451
+ destinationName,
452
+ });
453
+
454
+ continue;
455
+ } else if (typeof destinationOverride === "object") {
456
+ // conditions
457
+ if (destinationOverride.conditions) {
458
+ const isMatched = await this.conditionsChecker.allAreMatched(
459
+ destinationOverride.conditions,
460
+ {
461
+ eventName,
462
+ eventLevel,
463
+ payload: transportBody,
464
+ },
465
+ );
466
+
467
+ if (!isMatched) {
468
+ this.logger.debug(`Destination conditions not matched for event`, {
469
+ eventName,
470
+ destinationName,
471
+ });
472
+
473
+ continue;
474
+ }
475
+
476
+ // sample
477
+ if (destinationOverride.sample) {
478
+ const sampleResult = await this.bucketer.isSampled(destinationOverride.sample, {
479
+ eventName,
480
+ eventLevel,
481
+ payload: transportBody,
482
+ });
483
+
484
+ if (!sampleResult.isSampled) {
485
+ this.logger.debug(`Destination sample not matched for event`, {
486
+ eventName,
487
+ destinationName,
488
+ matchedSample: sampleResult.matchedSample,
489
+ bucketedNumber: sampleResult.bucketedNumber,
490
+ bucketKey: sampleResult.bucketKey,
491
+ });
492
+
493
+ continue;
494
+ }
495
+ }
496
+
497
+ // transform
498
+ if (destinationOverride.transforms) {
499
+ // @TODO: make sure this transformed value is only affecting the specific desired destination and not others
500
+ transportBody = await this.transformer.applyAll(
501
+ transformedValue,
502
+ destinationOverride.transforms,
503
+ {
504
+ eventName,
505
+ eventLevel,
506
+ payload: transportBody,
507
+ },
508
+ );
509
+ }
510
+ }
511
+ }
512
+ }
513
+
514
+ /**
515
+ * Destination itself
516
+ */
517
+
518
+ // conditions
519
+ if (destination.conditions) {
520
+ const isMatched = await this.conditionsChecker.allAreMatched(destination.conditions, {
521
+ eventName,
522
+ eventLevel,
523
+ payload: transformedValue,
524
+ });
525
+
526
+ if (!isMatched) {
527
+ this.logger.debug(`Destination conditions not matched`, {
528
+ eventName,
529
+ destinationName,
530
+ });
531
+
532
+ continue;
533
+ }
534
+ }
535
+
536
+ // sample
537
+ if (destination.sample) {
538
+ const sampleResult = await this.bucketer.isSampled(destination.sample, {
539
+ eventName,
540
+ eventLevel,
541
+ payload: transportBody,
542
+ });
543
+
544
+ if (!sampleResult.isSampled) {
545
+ this.logger.debug(`Destination sample not matched`, {
546
+ eventName,
547
+ destinationName,
548
+ });
549
+
550
+ continue;
551
+ }
552
+ }
553
+
554
+ // transform
555
+ if (destination.transforms) {
556
+ transportBody = await this.transformer.applyAll(transportBody, destination.transforms, {
557
+ eventName,
558
+ eventLevel,
559
+ payload: transportBody,
560
+ destinationName,
561
+ attributes: this.attributesManager.getAttributesMap(), // @TODO: check if needed
562
+ });
563
+ }
564
+
565
+ // hand over to module for transporting
566
+ // @TODO: decide about "await" or not
567
+ // @TODO: batch
568
+ // @TODO: retry
569
+ await this.modulesManager.transport(
570
+ destination.transport,
571
+ destinationName,
572
+ eventName,
573
+ transportBody,
574
+ eventLevel,
575
+ );
576
+ }
577
+
578
+ return transformedValue;
579
+ }
580
+
581
+ track(eventName: EventName, value: Value) {
582
+ this.addToQueue({
583
+ type: "track",
584
+ name: eventName,
585
+ value,
586
+ });
587
+
588
+ this.processQueue();
589
+ }
590
+
591
+ /**
592
+ * Effect's state
593
+ */
594
+ getStateValue(name: EffectName) {
595
+ return this.effectsManager.getStateValue(name);
596
+ }
597
+
598
+ /**
599
+ * @TODO: implement
600
+ */
601
+ spawn() {
602
+ // create child instance here
603
+ }
604
+ }
605
+
606
+ export function createInstance(options: InstanceOptions = {}): Eventvisor {
607
+ return new Eventvisor(options);
608
+ }
package/src/logger.ts ADDED
@@ -0,0 +1,90 @@
1
+ export type LogLevel = "fatal" | "error" | "warn" | "info" | "debug";
2
+
3
+ export type LogMessage = string;
4
+
5
+ export interface LogDetails {
6
+ [key: string]: any;
7
+ }
8
+
9
+ export type LogHandler = (level: LogLevel, message: LogMessage, details?: LogDetails) => void;
10
+
11
+ export interface CreateLoggerOptions {
12
+ level?: LogLevel;
13
+ handler?: LogHandler;
14
+ }
15
+
16
+ export const loggerPrefix = "[Eventvisor]";
17
+
18
+ export const defaultLogHandler: LogHandler = function defaultLogHandler(
19
+ level,
20
+ message,
21
+ details = {},
22
+ ) {
23
+ let method = "log";
24
+
25
+ if (level === "info") {
26
+ method = "info";
27
+ } else if (level === "warn") {
28
+ method = "warn";
29
+ } else if (level === "error") {
30
+ method = "error";
31
+ }
32
+
33
+ console[method](loggerPrefix, message, details);
34
+ };
35
+
36
+ export class Logger {
37
+ static allLevels: LogLevel[] = [
38
+ "fatal",
39
+ "error",
40
+ "warn",
41
+ "info",
42
+
43
+ // not enabled by default
44
+ "debug",
45
+ ];
46
+
47
+ static defaultLevel: LogLevel = "info";
48
+
49
+ private level: LogLevel;
50
+ private handle: LogHandler;
51
+
52
+ constructor(options: CreateLoggerOptions) {
53
+ this.level = options.level || Logger.defaultLevel;
54
+ this.handle = options.handler || defaultLogHandler;
55
+ }
56
+
57
+ setLevel(level: LogLevel) {
58
+ this.level = level;
59
+ }
60
+
61
+ log(level: LogLevel, message: LogMessage, details?: LogDetails) {
62
+ const shouldHandle = Logger.allLevels.indexOf(this.level) >= Logger.allLevels.indexOf(level);
63
+
64
+ if (!shouldHandle) {
65
+ return;
66
+ }
67
+
68
+ this.handle(level, message, details);
69
+ }
70
+
71
+ debug(message: LogMessage, details?: LogDetails) {
72
+ this.log("debug", message, details);
73
+ }
74
+
75
+ info(message: LogMessage, details?: LogDetails) {
76
+ this.log("info", message, details);
77
+ }
78
+
79
+ warn(message: LogMessage, details?: LogDetails) {
80
+ this.log("warn", message, details);
81
+ }
82
+
83
+ error(message: LogMessage, details?: LogDetails) {
84
+ this.log("error", message, details);
85
+ }
86
+ }
87
+
88
+ export function createLogger(options: CreateLoggerOptions = {}): Logger {
89
+ return new Logger(options);
90
+ }