@launchdarkly/js-sdk-common 2.17.0 → 2.18.0

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 (40) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/cjs/AttributeReference.d.ts +1 -0
  3. package/dist/cjs/AttributeReference.d.ts.map +1 -1
  4. package/dist/cjs/Context.d.ts +9 -0
  5. package/dist/cjs/Context.d.ts.map +1 -1
  6. package/dist/cjs/api/subsystem/DataSystem/DataSource.d.ts +1 -1
  7. package/dist/cjs/index.cjs +129 -9
  8. package/dist/cjs/index.cjs.map +1 -1
  9. package/dist/cjs/internal/events/EventProcessor.d.ts +1 -1
  10. package/dist/cjs/internal/events/EventProcessor.d.ts.map +1 -1
  11. package/dist/cjs/internal/events/LDEventSummarizer.d.ts +32 -0
  12. package/dist/cjs/internal/events/LDEventSummarizer.d.ts.map +1 -0
  13. package/dist/cjs/internal/events/MultiEventSummarizer.d.ts +13 -0
  14. package/dist/cjs/internal/events/MultiEventSummarizer.d.ts.map +1 -0
  15. package/dist/cjs/internal/index.d.ts +1 -0
  16. package/dist/cjs/internal/index.d.ts.map +1 -1
  17. package/dist/cjs/internal/json/canonicalize.d.ts +10 -0
  18. package/dist/cjs/internal/json/canonicalize.d.ts.map +1 -0
  19. package/dist/cjs/internal/json/index.d.ts +2 -0
  20. package/dist/cjs/internal/json/index.d.ts.map +1 -0
  21. package/dist/esm/AttributeReference.d.ts +1 -0
  22. package/dist/esm/AttributeReference.d.ts.map +1 -1
  23. package/dist/esm/Context.d.ts +9 -0
  24. package/dist/esm/Context.d.ts.map +1 -1
  25. package/dist/esm/api/subsystem/DataSystem/DataSource.d.ts +1 -1
  26. package/dist/esm/index.mjs +129 -9
  27. package/dist/esm/index.mjs.map +1 -1
  28. package/dist/esm/internal/events/EventProcessor.d.ts +1 -1
  29. package/dist/esm/internal/events/EventProcessor.d.ts.map +1 -1
  30. package/dist/esm/internal/events/LDEventSummarizer.d.ts +32 -0
  31. package/dist/esm/internal/events/LDEventSummarizer.d.ts.map +1 -0
  32. package/dist/esm/internal/events/MultiEventSummarizer.d.ts +13 -0
  33. package/dist/esm/internal/events/MultiEventSummarizer.d.ts.map +1 -0
  34. package/dist/esm/internal/index.d.ts +1 -0
  35. package/dist/esm/internal/index.d.ts.map +1 -1
  36. package/dist/esm/internal/json/canonicalize.d.ts +10 -0
  37. package/dist/esm/internal/json/canonicalize.d.ts.map +1 -0
  38. package/dist/esm/internal/json/index.d.ts +2 -0
  39. package/dist/esm/internal/json/index.d.ts.map +1 -0
  40. package/package.json +1 -1
@@ -115,6 +115,9 @@ class AttributeReference {
115
115
  return (this.depth === other.depth &&
116
116
  this._components.every((value, index) => value === other.getComponent(index)));
117
117
  }
118
+ get components() {
119
+ return [...this._components];
120
+ }
118
121
  }
119
122
  /**
120
123
  * For use as invalid references when deserializing Flag/Segment data.
@@ -314,6 +317,41 @@ function isLegacyUser(context) {
314
317
  return !('kind' in context) || context.kind === null || context.kind === undefined;
315
318
  }
316
319
 
320
+ /**
321
+ * Given some object to serialize product a canonicalized JSON string.
322
+ * https://www.rfc-editor.org/rfc/rfc8785.html
323
+ *
324
+ * We do not support custom toJSON methods on objects. Objects should be limited to basic types.
325
+ *
326
+ * @param object The object to serialize.
327
+ */
328
+ function canonicalize(object, visited = []) {
329
+ // For JavaScript the default JSON serialization will produce canonicalized output for basic types.
330
+ if (object === null || typeof object !== 'object') {
331
+ return JSON.stringify(object);
332
+ }
333
+ if (visited.includes(object)) {
334
+ throw new Error('Cycle detected');
335
+ }
336
+ if (Array.isArray(object)) {
337
+ const values = object
338
+ .map((item) => canonicalize(item, [...visited, object]))
339
+ .map((item) => (item === undefined ? 'null' : item));
340
+ return `[${values.join(',')}]`;
341
+ }
342
+ const values = Object.keys(object)
343
+ .sort()
344
+ .map((key) => {
345
+ const value = canonicalize(object[key], [...visited, object]);
346
+ if (value !== undefined) {
347
+ return `${JSON.stringify(key)}:${value}`;
348
+ }
349
+ return undefined;
350
+ })
351
+ .filter((item) => item !== undefined);
352
+ return `{${values.join(',')}}`;
353
+ }
354
+
317
355
  // The general strategy for the context is to transform the passed in context
318
356
  // as little as possible. We do convert the legacy users to a single kind
319
357
  // context, but we do not translate all passed contexts into a rigid structure.
@@ -688,6 +726,28 @@ class Context {
688
726
  get legacy() {
689
727
  return this._wasLegacy;
690
728
  }
729
+ /**
730
+ * Get the serialized canonical JSON for this context. This is not filtered for use in events.
731
+ *
732
+ * This method will cache the result.
733
+ *
734
+ * @returns The serialized canonical JSON or undefined if it cannot be serialized.
735
+ */
736
+ canonicalUnfilteredJson() {
737
+ if (!this.valid) {
738
+ return undefined;
739
+ }
740
+ if (this._cachedCanonicalJson) {
741
+ return this._cachedCanonicalJson;
742
+ }
743
+ try {
744
+ this._cachedCanonicalJson = canonicalize(Context.toLDContext(this));
745
+ }
746
+ catch {
747
+ // Indicated by undefined being returned.
748
+ }
749
+ return this._cachedCanonicalJson;
750
+ }
691
751
  }
692
752
  Context.UserKind = DEFAULT_KIND;
693
753
 
@@ -2351,7 +2411,9 @@ function counterKey(event) {
2351
2411
  * @internal
2352
2412
  */
2353
2413
  class EventSummarizer {
2354
- constructor() {
2414
+ constructor(_singleContext = false, _contextFilter) {
2415
+ this._singleContext = _singleContext;
2416
+ this._contextFilter = _contextFilter;
2355
2417
  this._startDate = 0;
2356
2418
  this._endDate = 0;
2357
2419
  this._counters = {};
@@ -2359,6 +2421,9 @@ class EventSummarizer {
2359
2421
  }
2360
2422
  summarizeEvent(event) {
2361
2423
  if (isFeature(event) && !event.excludeFromSummaries) {
2424
+ if (!this._context) {
2425
+ this._context = event.context;
2426
+ }
2362
2427
  const countKey = counterKey(event);
2363
2428
  const counter = this._counters[countKey];
2364
2429
  let kinds = this._contextKinds[event.key];
@@ -2408,14 +2473,19 @@ class EventSummarizer {
2408
2473
  flagSummary.counters.push(counterOut);
2409
2474
  return acc;
2410
2475
  }, {});
2411
- return {
2476
+ const event = {
2412
2477
  startDate: this._startDate,
2413
2478
  endDate: this._endDate,
2414
2479
  features,
2415
2480
  kind: 'summary',
2481
+ context: this._context !== undefined && this._singleContext
2482
+ ? this._contextFilter?.filter(this._context)
2483
+ : undefined,
2416
2484
  };
2485
+ this._clearSummary();
2486
+ return event;
2417
2487
  }
2418
- clearSummary() {
2488
+ _clearSummary() {
2419
2489
  this._startDate = 0;
2420
2490
  this._endDate = 0;
2421
2491
  this._counters = {};
@@ -2430,6 +2500,38 @@ class LDInvalidSDKKeyError extends Error {
2430
2500
  }
2431
2501
  }
2432
2502
 
2503
+ class MultiEventSummarizer {
2504
+ constructor(_contextFilter, _logger) {
2505
+ this._contextFilter = _contextFilter;
2506
+ this._logger = _logger;
2507
+ this._summarizers = {};
2508
+ }
2509
+ summarizeEvent(event) {
2510
+ if (isFeature(event)) {
2511
+ const key = event.context.canonicalUnfilteredJson();
2512
+ if (!key) {
2513
+ if (event.context.valid) {
2514
+ // The context appeared valid, but it could not be hashed.
2515
+ // This is likely because of a cycle in the data.
2516
+ this._logger?.error('Unable to serialize context, likely the context contains a cycle.');
2517
+ }
2518
+ return;
2519
+ }
2520
+ let summarizer = this._summarizers[key];
2521
+ if (!summarizer) {
2522
+ this._summarizers[key] = new EventSummarizer(true, this._contextFilter);
2523
+ summarizer = this._summarizers[key];
2524
+ }
2525
+ summarizer.summarizeEvent(event);
2526
+ }
2527
+ }
2528
+ getSummaries() {
2529
+ const summarizersToFlush = this._summarizers;
2530
+ this._summarizers = {};
2531
+ return Object.values(summarizersToFlush).map((summarizer) => summarizer.getSummary());
2532
+ }
2533
+ }
2534
+
2433
2535
  /**
2434
2536
  * The contents of this file are for event sampling. They are not used for
2435
2537
  * any purpose requiring cryptographic security.
@@ -2450,12 +2552,14 @@ function shouldSample(ratio) {
2450
2552
  return Math.floor(Math.random() * truncated) === 0;
2451
2553
  }
2452
2554
 
2555
+ function isMultiEventSummarizer(summarizer) {
2556
+ return summarizer.getSummaries !== undefined;
2557
+ }
2453
2558
  class EventProcessor {
2454
- constructor(_config, clientContext, baseHeaders, _contextDeduplicator, _diagnosticsManager, start = true) {
2559
+ constructor(_config, clientContext, baseHeaders, _contextDeduplicator, _diagnosticsManager, start = true, summariesPerContext = false) {
2455
2560
  this._config = _config;
2456
2561
  this._contextDeduplicator = _contextDeduplicator;
2457
2562
  this._diagnosticsManager = _diagnosticsManager;
2458
- this._summarizer = new EventSummarizer();
2459
2563
  this._queue = [];
2460
2564
  this._lastKnownPastTime = 0;
2461
2565
  this._droppedEvents = 0;
@@ -2468,6 +2572,12 @@ class EventProcessor {
2468
2572
  this._logger = clientContext.basicConfiguration.logger;
2469
2573
  this._eventSender = new EventSender(clientContext, baseHeaders);
2470
2574
  this._contextFilter = new ContextFilter(_config.allAttributesPrivate, _config.privateAttributes.map((ref) => new AttributeReference(ref)));
2575
+ if (summariesPerContext) {
2576
+ this._summarizer = new MultiEventSummarizer(this._contextFilter, this._logger);
2577
+ }
2578
+ else {
2579
+ this._summarizer = new EventSummarizer();
2580
+ }
2471
2581
  if (start) {
2472
2582
  this.start();
2473
2583
  }
@@ -2519,10 +2629,19 @@ class EventProcessor {
2519
2629
  }
2520
2630
  const eventsToFlush = this._queue;
2521
2631
  this._queue = [];
2522
- const summary = this._summarizer.getSummary();
2523
- this._summarizer.clearSummary();
2524
- if (Object.keys(summary.features).length) {
2525
- eventsToFlush.push(summary);
2632
+ if (isMultiEventSummarizer(this._summarizer)) {
2633
+ const summaries = this._summarizer.getSummaries();
2634
+ summaries.forEach((summary) => {
2635
+ if (Object.keys(summary.features).length) {
2636
+ eventsToFlush.push(summary);
2637
+ }
2638
+ });
2639
+ }
2640
+ else {
2641
+ const summary = this._summarizer.getSummary();
2642
+ if (Object.keys(summary.features).length) {
2643
+ eventsToFlush.push(summary);
2644
+ }
2526
2645
  }
2527
2646
  if (!eventsToFlush.length) {
2528
2647
  return;
@@ -3113,6 +3232,7 @@ var index = /*#__PURE__*/Object.freeze({
3113
3232
  NullEventProcessor: NullEventProcessor,
3114
3233
  PayloadProcessor: PayloadProcessor,
3115
3234
  PayloadStreamReader: PayloadStreamReader,
3235
+ canonicalize: canonicalize,
3116
3236
  initMetadataFromHeaders: initMetadataFromHeaders,
3117
3237
  isLegacyUser: isLegacyUser,
3118
3238
  isMultiKind: isMultiKind,