Skip to content

Commit c9fa5c4

Browse files
authored
feat: Add support for per-context summary events. (#859)
Adds support for per-context summary events. Per-context summary events allow for enhanced analytics to know which contexts evaluate which flags, even when those flags are not part of an experiment.
1 parent 20170c6 commit c9fa5c4

30 files changed

+913
-51
lines changed

.github/workflows/browser.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,4 @@ jobs:
4141
target_file: 'packages/sdk/browser/dist/index.js'
4242
package_name: '@launchdarkly/js-client-sdk'
4343
pr_number: ${{ github.event.number }}
44-
size_limit: 21000
44+
size_limit: 25000

packages/sdk/browser/contract-tests/entity/src/TestHarnessWebSocket.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export default class TestHarnessWebSocket {
4242
'anonymous-redaction',
4343
'strongly-typed',
4444
'client-prereq-events',
45+
'client-per-context-summaries',
4546
'track-hooks',
4647
];
4748

packages/sdk/browser/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
"eslint-plugin-jest": "^27.6.3",
7272
"eslint-plugin-prettier": "^5.0.0",
7373
"jest": "^29.7.0",
74-
"jest-environment-jsdom": "^29.7.0",
74+
"jest-environment-jsdom": "29.7.0",
7575
"prettier": "^3.0.0",
7676
"rimraf": "^5.0.5",
7777
"ts-jest": "^29.1.1",

packages/shared/common/__tests__/Context.test.ts

Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import AttributeReference from '../src/AttributeReference';
22
import Context from '../src/Context';
3+
import { setupCrypto } from './setupCrypto';
34

45
// A sample of invalid characters.
56
const invalidSampleChars = [
@@ -325,3 +326,346 @@ describe('given a multi context', () => {
325326
expect(Context.toLDContext(input)).toEqual(expected);
326327
});
327328
});
329+
330+
describe('given mock crypto', () => {
331+
const crypto = setupCrypto();
332+
333+
it('hashes two equal contexts the same', async () => {
334+
const a = Context.fromLDContext({
335+
kind: 'multi',
336+
org: {
337+
key: 'testKey',
338+
name: 'testName',
339+
cat: 'calico',
340+
dog: 'lab',
341+
anonymous: true,
342+
_meta: {
343+
privateAttributes: ['/a/b/c', 'cat', 'custom/dog'],
344+
},
345+
},
346+
customer: {
347+
key: 'testKey',
348+
name: 'testName',
349+
bird: 'party parrot',
350+
chicken: 'hen',
351+
},
352+
});
353+
354+
const b = Context.fromLDContext({
355+
kind: 'multi',
356+
org: {
357+
key: 'testKey',
358+
name: 'testName',
359+
cat: 'calico',
360+
dog: 'lab',
361+
anonymous: true,
362+
_meta: {
363+
privateAttributes: ['/a/b/c', 'cat', 'custom/dog'],
364+
},
365+
},
366+
customer: {
367+
key: 'testKey',
368+
name: 'testName',
369+
bird: 'party parrot',
370+
chicken: 'hen',
371+
},
372+
});
373+
expect(await a.hash(crypto)).toEqual(await b.hash(crypto));
374+
});
375+
376+
it('handles shared references without getting stuck', async () => {
377+
const sharedObject = { value: 'shared' };
378+
const context = Context.fromLDContext({
379+
kind: 'multi',
380+
org: {
381+
key: 'testKey',
382+
shared: sharedObject,
383+
},
384+
user: {
385+
key: 'testKey',
386+
shared: sharedObject,
387+
},
388+
});
389+
390+
const hash = await context.hash(crypto);
391+
expect(hash).toBeDefined();
392+
});
393+
394+
it('returns undefined for contexts with cycles', async () => {
395+
const cyclicObject: any = { value: 'cyclic' };
396+
cyclicObject.self = cyclicObject;
397+
398+
const context = Context.fromLDContext({
399+
kind: 'user',
400+
key: 'testKey',
401+
cyclic: cyclicObject,
402+
});
403+
404+
expect(await context.hash(crypto)).toBeUndefined();
405+
});
406+
407+
it('handles nested objects correctly', async () => {
408+
const context = Context.fromLDContext({
409+
kind: 'user',
410+
key: 'testKey',
411+
nested: {
412+
level1: {
413+
level2: {
414+
value: 'deep',
415+
},
416+
},
417+
},
418+
});
419+
420+
const hash = await context.hash(crypto);
421+
expect(hash).toBeDefined();
422+
});
423+
424+
it('handles arrays correctly', async () => {
425+
const context = Context.fromLDContext({
426+
kind: 'user',
427+
key: 'testKey',
428+
array: [1, 2, 3],
429+
nestedArray: [
430+
[1, 2],
431+
[3, 4],
432+
],
433+
});
434+
435+
const hash = await context.hash(crypto);
436+
expect(hash).toBeDefined();
437+
});
438+
439+
it('handles primitive values correctly', async () => {
440+
const context = Context.fromLDContext({
441+
kind: 'user',
442+
key: 'testKey',
443+
string: 'test',
444+
number: 42,
445+
boolean: true,
446+
nullValue: null,
447+
undefinedValue: undefined,
448+
});
449+
450+
const hash = await context.hash(crypto);
451+
expect(hash).toBeDefined();
452+
});
453+
454+
it('includes private attributes in hash calculation', async () => {
455+
const baseContext = {
456+
kind: 'user',
457+
key: 'testKey',
458+
name: 'testName',
459+
nested: {
460+
value: 'testValue',
461+
},
462+
};
463+
464+
const contextWithPrivate = Context.fromLDContext({
465+
...baseContext,
466+
_meta: {
467+
privateAttributes: ['name', 'nested/value'],
468+
},
469+
});
470+
471+
const contextWithoutPrivate = Context.fromLDContext(baseContext);
472+
473+
const hashWithPrivate = await contextWithPrivate.hash(crypto);
474+
const hashWithoutPrivate = await contextWithoutPrivate.hash(crypto);
475+
476+
// The hashes should be different because private attributes are included in the hash
477+
expect(hashWithPrivate).not.toEqual(hashWithoutPrivate);
478+
});
479+
480+
it('uses the keys the keys of attributes in the hash', async () => {
481+
const a = Context.fromLDContext({
482+
kind: 'user',
483+
key: 'testKey',
484+
a: 'b',
485+
});
486+
487+
const b = Context.fromLDContext({
488+
kind: 'user',
489+
key: 'testKey',
490+
b: 'b',
491+
});
492+
493+
const hashA = await a.hash(crypto);
494+
const hashB = await b.hash(crypto);
495+
expect(hashA).not.toBe(hashB);
496+
});
497+
498+
it('uses the keys of nested objects inside the hash', async () => {
499+
const a = Context.fromLDContext({
500+
kind: 'user',
501+
key: 'testKey',
502+
nested: {
503+
level1: {
504+
level2: {
505+
value: 'deep',
506+
},
507+
},
508+
},
509+
});
510+
511+
const b = Context.fromLDContext({
512+
kind: 'user',
513+
key: 'testKey',
514+
nested: {
515+
sub1: {
516+
sub2: {
517+
value: 'deep',
518+
},
519+
},
520+
},
521+
});
522+
523+
const hashA = await a.hash(crypto);
524+
const hashB = await b.hash(crypto);
525+
expect(hashA).not.toBe(hashB);
526+
});
527+
528+
it('it uses the values of nested array in calculations', async () => {
529+
const a = Context.fromLDContext({
530+
kind: 'user',
531+
key: 'testKey',
532+
array: [1, 2, 3],
533+
nestedArray: [
534+
[1, 2],
535+
[3, 4],
536+
],
537+
});
538+
539+
const b = Context.fromLDContext({
540+
kind: 'user',
541+
key: 'testKey',
542+
array: [1, 2, 3],
543+
nestedArray: [
544+
[2, 1],
545+
[3, 4],
546+
],
547+
});
548+
549+
const hashA = await a.hash(crypto);
550+
const hashB = await b.hash(crypto);
551+
expect(hashA).not.toBe(hashB);
552+
});
553+
554+
it('uses the values of nested objects inside the hash', async () => {
555+
const a = Context.fromLDContext({
556+
kind: 'user',
557+
key: 'testKey',
558+
nested: {
559+
level1: {
560+
level2: {
561+
value: 'deep',
562+
},
563+
},
564+
},
565+
});
566+
567+
const b = Context.fromLDContext({
568+
kind: 'user',
569+
key: 'testKey',
570+
nested: {
571+
level1: {
572+
level2: {
573+
value: 'deeper',
574+
},
575+
},
576+
},
577+
});
578+
579+
const hashA = await a.hash(crypto);
580+
const hashB = await b.hash(crypto);
581+
expect(hashA).not.toBe(hashB);
582+
});
583+
584+
it('hashes _meta in attributes', async () => {
585+
const a = Context.fromLDContext({
586+
kind: 'user',
587+
key: 'testKey',
588+
nested: {
589+
level1: {
590+
level2: {
591+
_meta: { test: 'a' },
592+
},
593+
},
594+
},
595+
});
596+
597+
const b = Context.fromLDContext({
598+
kind: 'user',
599+
key: 'testKey',
600+
nested: {
601+
level1: {
602+
level2: {
603+
_meta: { test: 'b' },
604+
},
605+
},
606+
},
607+
});
608+
609+
const hashA = await a.hash(crypto);
610+
const hashB = await b.hash(crypto);
611+
expect(hashA).not.toBe(hashB);
612+
});
613+
614+
it('produces the same value for the given context', async () => {
615+
// This isn't so much a test as it is a detection of change.
616+
// If this test failed, and you didn't expect it, then you probably need to make sure your
617+
// change makes sense.
618+
const complexContext = Context.fromLDContext({
619+
kind: 'multi',
620+
org: {
621+
key: 'testKey',
622+
name: 'testName',
623+
cat: 'calico',
624+
dog: 'lab',
625+
anonymous: true,
626+
nestedArray: [
627+
[1, 2],
628+
[3, 4],
629+
],
630+
_meta: {
631+
privateAttributes: ['/a/b/c', 'cat', 'custom/dog'],
632+
},
633+
},
634+
customer: {
635+
key: 'testKey',
636+
name: 'testName',
637+
bird: 'party parrot',
638+
chicken: 'hen',
639+
nested: {
640+
level1: {
641+
level2: {
642+
value: 'deep',
643+
_meta: { thisShouldBeInTheHash: true },
644+
},
645+
},
646+
},
647+
},
648+
});
649+
expect(await complexContext.hash(crypto)).toBe(
650+
'{"_contexts":{"customer":{"bird":"party parrot","chicken":"hen","key":"testKey","name":"testName","nested":{"level1":{"level2":{"_meta":{"thisShouldBeInTheHash":true},"value":"deep"}}}},"org":{"_meta":{"privateAttributes":["/a/b/c","cat","custom/dog"]},"anonymous":true,"cat":"calico","dog":"lab","key":"testKey","name":"testName","nestedArray":[[1,2],[3,4]]}},"_isMulti":true,"_isUser":false,"_privateAttributeReferences":{"customer":[],"org":[{"_components":["a","b","c"],"isValid":true,"redactionName":"/a/b/c"},{"_components":["cat"],"isValid":true,"redactionName":"cat"},{"_components":["custom/dog"],"isValid":true,"redactionName":"custom/dog"}]},"_wasLegacy":false,"kind":"multi","valid":true}',
651+
);
652+
});
653+
654+
it('collisiontest', async () => {
655+
const a = Context.fromLDContext({
656+
kind: 'user',
657+
key: 'bob',
658+
a: 'bcd',
659+
});
660+
661+
const b = Context.fromLDContext({
662+
kind: 'user',
663+
key: 'bob',
664+
a: { b: { c: 'd' } },
665+
});
666+
667+
const hashA = await a.hash(crypto);
668+
const hashB = await b.hash(crypto);
669+
expect(hashA).not.toBe(hashB);
670+
});
671+
});

0 commit comments

Comments
 (0)