@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
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to `@launchdarkly/js-sdk-common` will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org).
4
4
 
5
+ ## [2.18.0](https://github.com/launchdarkly/js-core/compare/js-sdk-common-v2.17.0...js-sdk-common-v2.18.0) (2025-05-21)
6
+
7
+
8
+ ### Features
9
+
10
+ * Add support for per-context summary events. ([#859](https://github.com/launchdarkly/js-core/issues/859)) ([c9fa5c4](https://github.com/launchdarkly/js-core/commit/c9fa5c45f3ac2cbaad2f2e6312d5231c3f671d98))
11
+
5
12
  ## [2.17.0](https://github.com/launchdarkly/js-core/compare/js-sdk-common-v2.16.0...js-sdk-common-v2.17.0) (2025-04-29)
6
13
 
7
14
 
@@ -31,5 +31,6 @@ export default class AttributeReference {
31
31
  get depth(): number;
32
32
  get isKind(): boolean;
33
33
  compare(other: AttributeReference): boolean;
34
+ get components(): string[];
34
35
  }
35
36
  //# sourceMappingURL=AttributeReference.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AttributeReference.d.ts","sourceRoot":"","sources":["../src/AttributeReference.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAiChE,MAAM,CAAC,OAAO,OAAO,kBAAkB;IACrC,SAAgB,OAAO,UAAC;IAExB;;;OAGG;IACH,SAAgB,aAAa,SAAC;IAE9B;;OAEG;IACH,gBAAuB,gBAAgB,qBAA8B;IAErE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAW;IAEvC;;;;;;;;;;;;;OAaG;gBACgB,YAAY,EAAE,MAAM,EAAE,OAAO,GAAE,OAAe;IAkC1D,GAAG,CAAC,MAAM,EAAE,eAAe;IA+B3B,YAAY,CAAC,KAAK,EAAE,MAAM;IAIjC,IAAW,KAAK,WAEf;IAED,IAAW,MAAM,IAAI,OAAO,CAE3B;IAEM,OAAO,CAAC,KAAK,EAAE,kBAAkB;CAMzC"}
1
+ {"version":3,"file":"AttributeReference.d.ts","sourceRoot":"","sources":["../src/AttributeReference.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAiChE,MAAM,CAAC,OAAO,OAAO,kBAAkB;IACrC,SAAgB,OAAO,UAAC;IAExB;;;OAGG;IACH,SAAgB,aAAa,SAAC;IAE9B;;OAEG;IACH,gBAAuB,gBAAgB,qBAA8B;IAErE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAW;IAEvC;;;;;;;;;;;;;OAaG;gBACgB,YAAY,EAAE,MAAM,EAAE,OAAO,GAAE,OAAe;IAkC1D,GAAG,CAAC,MAAM,EAAE,eAAe;IA+B3B,YAAY,CAAC,KAAK,EAAE,MAAM;IAIjC,IAAW,KAAK,WAEf;IAED,IAAW,MAAM,IAAI,OAAO,CAE3B;IAEM,OAAO,CAAC,KAAK,EAAE,kBAAkB;IAOxC,IAAW,UAAU,aAEpB;CACF"}
@@ -12,6 +12,7 @@ export default class Context {
12
12
  private _wasLegacy;
13
13
  private _contexts;
14
14
  private _privateAttributeReferences?;
15
+ private _cachedCanonicalJson?;
15
16
  readonly kind: string;
16
17
  /**
17
18
  * Is this a valid context. If a valid context cannot be created, then this flag will be true.
@@ -91,5 +92,13 @@ export default class Context {
91
92
  */
92
93
  getContexts(): [string, LDContextCommon][];
93
94
  get legacy(): boolean;
95
+ /**
96
+ * Get the serialized canonical JSON for this context. This is not filtered for use in events.
97
+ *
98
+ * This method will cache the result.
99
+ *
100
+ * @returns The serialized canonical JSON or undefined if it cannot be serialized.
101
+ */
102
+ canonicalUnfilteredJson(): string | undefined;
94
103
  }
95
104
  //# sourceMappingURL=Context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Context.d.ts","sourceRoot":"","sources":["../src/Context.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,SAAS,EACT,eAAe,EAIhB,MAAM,OAAO,CAAC;AACf,OAAO,kBAAkB,MAAM,sBAAsB,CAAC;AAiJtD;;;;GAIG;AACH,MAAM,CAAC,OAAO,OAAO,OAAO;IAC1B,OAAO,CAAC,QAAQ,CAAC,CAAkB;IAEnC,OAAO,CAAC,QAAQ,CAAkB;IAElC,OAAO,CAAC,OAAO,CAAkB;IAEjC,OAAO,CAAC,UAAU,CAAkB;IAEpC,OAAO,CAAC,SAAS,CAAuC;IAExD,OAAO,CAAC,2BAA2B,CAAC,CAAuC;IAE3E,SAAgB,IAAI,EAAE,MAAM,CAAC;IAE7B;;;OAGG;IACH,SAAgB,KAAK,EAAE,OAAO,CAAC;IAE/B,SAAgB,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjC,MAAM,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAgB;IAEhD;;;;;;OAMG;IACH,OAAO;IAMP,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAI/B,OAAO,CAAC,MAAM,CAAC,oBAAoB;IAcnC,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAwDpC,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAyBrC,OAAO,CAAC,MAAM,CAAC,eAAe;IAgB9B;;;;;OAKG;WACW,aAAa,CAAC,OAAO,EAAE,SAAS,GAAG,OAAO;IAiBxD;;;;OAIG;WACW,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS;IAoBlE;;;;;OAKG;IACI,YAAY,CAAC,SAAS,EAAE,kBAAkB,EAAE,IAAI,GAAE,MAAqB,GAAG,GAAG,GAAG,SAAS;IAOhG;;;;OAIG;IACI,GAAG,CAAC,IAAI,GAAE,MAAqB,GAAG,MAAM,GAAG,SAAS;IAI3D;;OAEG;IACH,IAAW,WAAW,IAAI,OAAO,CAEhC;IAED;;OAEG;IACH,IAAW,YAAY,IAAI,MAAM,CAWhC;IAED;;OAEG;IACH,IAAW,KAAK,IAAI,MAAM,EAAE,CAK3B;IAED;;OAEG;IACH,IAAW,YAAY,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAWhD;IAED;;;;OAIG;IACI,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,EAAE;IAI5D;;;;;;OAMG;IACI,WAAW,IAAI,CAAC,MAAM,EAAE,eAAe,CAAC,EAAE;IAOjD,IAAW,MAAM,IAAI,OAAO,CAE3B;CACF"}
1
+ {"version":3,"file":"Context.d.ts","sourceRoot":"","sources":["../src/Context.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,SAAS,EACT,eAAe,EAIhB,MAAM,OAAO,CAAC;AACf,OAAO,kBAAkB,MAAM,sBAAsB,CAAC;AAkJtD;;;;GAIG;AACH,MAAM,CAAC,OAAO,OAAO,OAAO;IAC1B,OAAO,CAAC,QAAQ,CAAC,CAAkB;IAEnC,OAAO,CAAC,QAAQ,CAAkB;IAElC,OAAO,CAAC,OAAO,CAAkB;IAEjC,OAAO,CAAC,UAAU,CAAkB;IAEpC,OAAO,CAAC,SAAS,CAAuC;IAExD,OAAO,CAAC,2BAA2B,CAAC,CAAuC;IAE3E,OAAO,CAAC,oBAAoB,CAAC,CAAS;IAEtC,SAAgB,IAAI,EAAE,MAAM,CAAC;IAE7B;;;OAGG;IACH,SAAgB,KAAK,EAAE,OAAO,CAAC;IAE/B,SAAgB,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjC,MAAM,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAgB;IAEhD;;;;;;OAMG;IACH,OAAO;IAMP,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAI/B,OAAO,CAAC,MAAM,CAAC,oBAAoB;IAcnC,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAwDpC,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAyBrC,OAAO,CAAC,MAAM,CAAC,eAAe;IAgB9B;;;;;OAKG;WACW,aAAa,CAAC,OAAO,EAAE,SAAS,GAAG,OAAO;IAiBxD;;;;OAIG;WACW,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS;IAoBlE;;;;;OAKG;IACI,YAAY,CAAC,SAAS,EAAE,kBAAkB,EAAE,IAAI,GAAE,MAAqB,GAAG,GAAG,GAAG,SAAS;IAOhG;;;;OAIG;IACI,GAAG,CAAC,IAAI,GAAE,MAAqB,GAAG,MAAM,GAAG,SAAS;IAI3D;;OAEG;IACH,IAAW,WAAW,IAAI,OAAO,CAEhC;IAED;;OAEG;IACH,IAAW,YAAY,IAAI,MAAM,CAWhC;IAED;;OAEG;IACH,IAAW,KAAK,IAAI,MAAM,EAAE,CAK3B;IAED;;OAEG;IACH,IAAW,YAAY,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAWhD;IAED;;;;OAIG;IACI,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,EAAE;IAI5D;;;;;;OAMG;IACI,WAAW,IAAI,CAAC,MAAM,EAAE,eAAe,CAAC,EAAE;IAOjD,IAAW,MAAM,IAAI,OAAO,CAE3B;IAED;;;;;;OAMG;IACI,uBAAuB,IAAI,MAAM,GAAG,SAAS;CAcrD"}
@@ -13,7 +13,7 @@ export interface DataSource {
13
13
  */
14
14
  start(dataCallback: (basis: boolean, data: any) => void, statusCallback: (status: DataSourceState, err?: any) => void): void;
15
15
  /**
16
- * May be called any number of times, if already stopped, has no effect.
16
+ * May be called any number of times, if already stopped, has no effect. Datasource will not make any additional callbacks after stop returns.
17
17
  */
18
18
  stop(): void;
19
19
  }
@@ -117,6 +117,9 @@ class AttributeReference {
117
117
  return (this.depth === other.depth &&
118
118
  this._components.every((value, index) => value === other.getComponent(index)));
119
119
  }
120
+ get components() {
121
+ return [...this._components];
122
+ }
120
123
  }
121
124
  /**
122
125
  * For use as invalid references when deserializing Flag/Segment data.
@@ -316,6 +319,41 @@ function isLegacyUser(context) {
316
319
  return !('kind' in context) || context.kind === null || context.kind === undefined;
317
320
  }
318
321
 
322
+ /**
323
+ * Given some object to serialize product a canonicalized JSON string.
324
+ * https://www.rfc-editor.org/rfc/rfc8785.html
325
+ *
326
+ * We do not support custom toJSON methods on objects. Objects should be limited to basic types.
327
+ *
328
+ * @param object The object to serialize.
329
+ */
330
+ function canonicalize(object, visited = []) {
331
+ // For JavaScript the default JSON serialization will produce canonicalized output for basic types.
332
+ if (object === null || typeof object !== 'object') {
333
+ return JSON.stringify(object);
334
+ }
335
+ if (visited.includes(object)) {
336
+ throw new Error('Cycle detected');
337
+ }
338
+ if (Array.isArray(object)) {
339
+ const values = object
340
+ .map((item) => canonicalize(item, [...visited, object]))
341
+ .map((item) => (item === undefined ? 'null' : item));
342
+ return `[${values.join(',')}]`;
343
+ }
344
+ const values = Object.keys(object)
345
+ .sort()
346
+ .map((key) => {
347
+ const value = canonicalize(object[key], [...visited, object]);
348
+ if (value !== undefined) {
349
+ return `${JSON.stringify(key)}:${value}`;
350
+ }
351
+ return undefined;
352
+ })
353
+ .filter((item) => item !== undefined);
354
+ return `{${values.join(',')}}`;
355
+ }
356
+
319
357
  // The general strategy for the context is to transform the passed in context
320
358
  // as little as possible. We do convert the legacy users to a single kind
321
359
  // context, but we do not translate all passed contexts into a rigid structure.
@@ -690,6 +728,28 @@ class Context {
690
728
  get legacy() {
691
729
  return this._wasLegacy;
692
730
  }
731
+ /**
732
+ * Get the serialized canonical JSON for this context. This is not filtered for use in events.
733
+ *
734
+ * This method will cache the result.
735
+ *
736
+ * @returns The serialized canonical JSON or undefined if it cannot be serialized.
737
+ */
738
+ canonicalUnfilteredJson() {
739
+ if (!this.valid) {
740
+ return undefined;
741
+ }
742
+ if (this._cachedCanonicalJson) {
743
+ return this._cachedCanonicalJson;
744
+ }
745
+ try {
746
+ this._cachedCanonicalJson = canonicalize(Context.toLDContext(this));
747
+ }
748
+ catch {
749
+ // Indicated by undefined being returned.
750
+ }
751
+ return this._cachedCanonicalJson;
752
+ }
693
753
  }
694
754
  Context.UserKind = DEFAULT_KIND;
695
755
 
@@ -2353,7 +2413,9 @@ function counterKey(event) {
2353
2413
  * @internal
2354
2414
  */
2355
2415
  class EventSummarizer {
2356
- constructor() {
2416
+ constructor(_singleContext = false, _contextFilter) {
2417
+ this._singleContext = _singleContext;
2418
+ this._contextFilter = _contextFilter;
2357
2419
  this._startDate = 0;
2358
2420
  this._endDate = 0;
2359
2421
  this._counters = {};
@@ -2361,6 +2423,9 @@ class EventSummarizer {
2361
2423
  }
2362
2424
  summarizeEvent(event) {
2363
2425
  if (isFeature(event) && !event.excludeFromSummaries) {
2426
+ if (!this._context) {
2427
+ this._context = event.context;
2428
+ }
2364
2429
  const countKey = counterKey(event);
2365
2430
  const counter = this._counters[countKey];
2366
2431
  let kinds = this._contextKinds[event.key];
@@ -2410,14 +2475,19 @@ class EventSummarizer {
2410
2475
  flagSummary.counters.push(counterOut);
2411
2476
  return acc;
2412
2477
  }, {});
2413
- return {
2478
+ const event = {
2414
2479
  startDate: this._startDate,
2415
2480
  endDate: this._endDate,
2416
2481
  features,
2417
2482
  kind: 'summary',
2483
+ context: this._context !== undefined && this._singleContext
2484
+ ? this._contextFilter?.filter(this._context)
2485
+ : undefined,
2418
2486
  };
2487
+ this._clearSummary();
2488
+ return event;
2419
2489
  }
2420
- clearSummary() {
2490
+ _clearSummary() {
2421
2491
  this._startDate = 0;
2422
2492
  this._endDate = 0;
2423
2493
  this._counters = {};
@@ -2432,6 +2502,38 @@ class LDInvalidSDKKeyError extends Error {
2432
2502
  }
2433
2503
  }
2434
2504
 
2505
+ class MultiEventSummarizer {
2506
+ constructor(_contextFilter, _logger) {
2507
+ this._contextFilter = _contextFilter;
2508
+ this._logger = _logger;
2509
+ this._summarizers = {};
2510
+ }
2511
+ summarizeEvent(event) {
2512
+ if (isFeature(event)) {
2513
+ const key = event.context.canonicalUnfilteredJson();
2514
+ if (!key) {
2515
+ if (event.context.valid) {
2516
+ // The context appeared valid, but it could not be hashed.
2517
+ // This is likely because of a cycle in the data.
2518
+ this._logger?.error('Unable to serialize context, likely the context contains a cycle.');
2519
+ }
2520
+ return;
2521
+ }
2522
+ let summarizer = this._summarizers[key];
2523
+ if (!summarizer) {
2524
+ this._summarizers[key] = new EventSummarizer(true, this._contextFilter);
2525
+ summarizer = this._summarizers[key];
2526
+ }
2527
+ summarizer.summarizeEvent(event);
2528
+ }
2529
+ }
2530
+ getSummaries() {
2531
+ const summarizersToFlush = this._summarizers;
2532
+ this._summarizers = {};
2533
+ return Object.values(summarizersToFlush).map((summarizer) => summarizer.getSummary());
2534
+ }
2535
+ }
2536
+
2435
2537
  /**
2436
2538
  * The contents of this file are for event sampling. They are not used for
2437
2539
  * any purpose requiring cryptographic security.
@@ -2452,12 +2554,14 @@ function shouldSample(ratio) {
2452
2554
  return Math.floor(Math.random() * truncated) === 0;
2453
2555
  }
2454
2556
 
2557
+ function isMultiEventSummarizer(summarizer) {
2558
+ return summarizer.getSummaries !== undefined;
2559
+ }
2455
2560
  class EventProcessor {
2456
- constructor(_config, clientContext, baseHeaders, _contextDeduplicator, _diagnosticsManager, start = true) {
2561
+ constructor(_config, clientContext, baseHeaders, _contextDeduplicator, _diagnosticsManager, start = true, summariesPerContext = false) {
2457
2562
  this._config = _config;
2458
2563
  this._contextDeduplicator = _contextDeduplicator;
2459
2564
  this._diagnosticsManager = _diagnosticsManager;
2460
- this._summarizer = new EventSummarizer();
2461
2565
  this._queue = [];
2462
2566
  this._lastKnownPastTime = 0;
2463
2567
  this._droppedEvents = 0;
@@ -2470,6 +2574,12 @@ class EventProcessor {
2470
2574
  this._logger = clientContext.basicConfiguration.logger;
2471
2575
  this._eventSender = new EventSender(clientContext, baseHeaders);
2472
2576
  this._contextFilter = new ContextFilter(_config.allAttributesPrivate, _config.privateAttributes.map((ref) => new AttributeReference(ref)));
2577
+ if (summariesPerContext) {
2578
+ this._summarizer = new MultiEventSummarizer(this._contextFilter, this._logger);
2579
+ }
2580
+ else {
2581
+ this._summarizer = new EventSummarizer();
2582
+ }
2473
2583
  if (start) {
2474
2584
  this.start();
2475
2585
  }
@@ -2521,10 +2631,19 @@ class EventProcessor {
2521
2631
  }
2522
2632
  const eventsToFlush = this._queue;
2523
2633
  this._queue = [];
2524
- const summary = this._summarizer.getSummary();
2525
- this._summarizer.clearSummary();
2526
- if (Object.keys(summary.features).length) {
2527
- eventsToFlush.push(summary);
2634
+ if (isMultiEventSummarizer(this._summarizer)) {
2635
+ const summaries = this._summarizer.getSummaries();
2636
+ summaries.forEach((summary) => {
2637
+ if (Object.keys(summary.features).length) {
2638
+ eventsToFlush.push(summary);
2639
+ }
2640
+ });
2641
+ }
2642
+ else {
2643
+ const summary = this._summarizer.getSummary();
2644
+ if (Object.keys(summary.features).length) {
2645
+ eventsToFlush.push(summary);
2646
+ }
2528
2647
  }
2529
2648
  if (!eventsToFlush.length) {
2530
2649
  return;
@@ -3115,6 +3234,7 @@ var index = /*#__PURE__*/Object.freeze({
3115
3234
  NullEventProcessor: NullEventProcessor,
3116
3235
  PayloadProcessor: PayloadProcessor,
3117
3236
  PayloadStreamReader: PayloadStreamReader,
3237
+ canonicalize: canonicalize,
3118
3238
  initMetadataFromHeaders: initMetadataFromHeaders,
3119
3239
  isLegacyUser: isLegacyUser,
3120
3240
  isMultiKind: isMultiKind,