Skip to content

feat: Add support for per-context summary events. #859

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7979d98
feat: Add support for multiple context event summaries.
kinyoklion Apr 11, 2025
c7b78af
Remove some console.logs.
kinyoklion Apr 14, 2025
2bc5b49
Some cleanup.
kinyoklion Apr 14, 2025
0dfd0b0
Extra blank line for multi summarizer tests.
kinyoklion Apr 14, 2025
30d03b8
More blank lines.
kinyoklion Apr 14, 2025
a990ee0
Lint
kinyoklion Apr 14, 2025
99f290d
Tweaks
kinyoklion Apr 14, 2025
79ea083
Don't mutate.
kinyoklion Apr 14, 2025
da3bcab
Merge branch 'main' into rlamb/prototype-multi-summary
kinyoklion Apr 16, 2025
83e3651
Revert browser-telemetry changes.
kinyoklion Apr 16, 2025
5dd8636
Re-organization.
kinyoklion Apr 16, 2025
142299c
Missed import
kinyoklion Apr 16, 2025
0daf2f8
Lint
kinyoklion Apr 16, 2025
f7fae6f
Remove optionality for context kinds.
kinyoklion Apr 17, 2025
1922be1
Add contract test capability for per-context summary events.
kinyoklion Apr 17, 2025
8c871b6
Lint
kinyoklion Apr 17, 2025
0dc4419
Lint
kinyoklion Apr 17, 2025
260692f
Remove double hashing of kind.
kinyoklion Apr 22, 2025
c28f996
Remove await from sync method.
kinyoklion Apr 23, 2025
8ba61a4
Async consistency.
kinyoklion Apr 23, 2025
995d976
Merge branch 'wtf/prototype-multi-summary' into rlamb/prototype-multi…
kinyoklion Apr 23, 2025
5a4d62f
Attempt to pin parse5.
kinyoklion Apr 23, 2025
df6fb05
Move resolutions to top level.
kinyoklion Apr 23, 2025
8f0569b
Merge branch 'main' into rlamb/prototype-multi-summary
kinyoklion Apr 23, 2025
5d75446
Extended test, fixed missing keys.
kinyoklion Apr 24, 2025
6c3f468
Additional tests.
kinyoklion Apr 24, 2025
5f8f992
Collision test.
kinyoklion Apr 25, 2025
89d2b75
Use canonicalized JSON.
kinyoklion May 15, 2025
5eee3b3
Merge branch 'main' into rlamb/prototype-multi-summary
kinyoklion May 15, 2025
233a731
Lint and package size increase.
kinyoklion May 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/browser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,4 @@ jobs:
target_file: 'packages/sdk/browser/dist/index.js'
package_name: '@launchdarkly/js-client-sdk'
pr_number: ${{ github.event.number }}
size_limit: 21000
size_limit: 25000
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both this PR and the client-side identify PR need a little breathing room. This PR exceeded the limit by 174 bytes.

Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export default class TestHarnessWebSocket {
'anonymous-redaction',
'strongly-typed',
'client-prereq-events',
'client-per-context-summaries',
'track-hooks',
];

Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"eslint-plugin-jest": "^27.6.3",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-environment-jsdom": "29.7.0",
"prettier": "^3.0.0",
"rimraf": "^5.0.5",
"ts-jest": "^29.1.1",
Expand Down
344 changes: 344 additions & 0 deletions packages/shared/common/__tests__/Context.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import AttributeReference from '../src/AttributeReference';
import Context from '../src/Context';
import { setupCrypto } from './setupCrypto';

// A sample of invalid characters.
const invalidSampleChars = [
Expand Down Expand Up @@ -325,3 +326,346 @@ describe('given a multi context', () => {
expect(Context.toLDContext(input)).toEqual(expected);
});
});

describe('given mock crypto', () => {
const crypto = setupCrypto();

it('hashes two equal contexts the same', async () => {
const a = Context.fromLDContext({
kind: 'multi',
org: {
key: 'testKey',
name: 'testName',
cat: 'calico',
dog: 'lab',
anonymous: true,
_meta: {
privateAttributes: ['/a/b/c', 'cat', 'custom/dog'],
},
},
customer: {
key: 'testKey',
name: 'testName',
bird: 'party parrot',
chicken: 'hen',
},
});

const b = Context.fromLDContext({
kind: 'multi',
org: {
key: 'testKey',
name: 'testName',
cat: 'calico',
dog: 'lab',
anonymous: true,
_meta: {
privateAttributes: ['/a/b/c', 'cat', 'custom/dog'],
},
},
customer: {
key: 'testKey',
name: 'testName',
bird: 'party parrot',
chicken: 'hen',
},
});
expect(await a.hash(crypto)).toEqual(await b.hash(crypto));
});

it('handles shared references without getting stuck', async () => {
const sharedObject = { value: 'shared' };
const context = Context.fromLDContext({
kind: 'multi',
org: {
key: 'testKey',
shared: sharedObject,
},
user: {
key: 'testKey',
shared: sharedObject,
},
});

const hash = await context.hash(crypto);
expect(hash).toBeDefined();
});

it('returns undefined for contexts with cycles', async () => {
const cyclicObject: any = { value: 'cyclic' };
cyclicObject.self = cyclicObject;

const context = Context.fromLDContext({
kind: 'user',
key: 'testKey',
cyclic: cyclicObject,
});

expect(await context.hash(crypto)).toBeUndefined();
});

it('handles nested objects correctly', async () => {
const context = Context.fromLDContext({
kind: 'user',
key: 'testKey',
nested: {
level1: {
level2: {
value: 'deep',
},
},
},
});

const hash = await context.hash(crypto);
expect(hash).toBeDefined();
});

it('handles arrays correctly', async () => {
const context = Context.fromLDContext({
kind: 'user',
key: 'testKey',
array: [1, 2, 3],
nestedArray: [
[1, 2],
[3, 4],
],
});

const hash = await context.hash(crypto);
expect(hash).toBeDefined();
});

it('handles primitive values correctly', async () => {
const context = Context.fromLDContext({
kind: 'user',
key: 'testKey',
string: 'test',
number: 42,
boolean: true,
nullValue: null,
undefinedValue: undefined,
});

const hash = await context.hash(crypto);
expect(hash).toBeDefined();
});

it('includes private attributes in hash calculation', async () => {
const baseContext = {
kind: 'user',
key: 'testKey',
name: 'testName',
nested: {
value: 'testValue',
},
};

const contextWithPrivate = Context.fromLDContext({
...baseContext,
_meta: {
privateAttributes: ['name', 'nested/value'],
},
});

const contextWithoutPrivate = Context.fromLDContext(baseContext);

const hashWithPrivate = await contextWithPrivate.hash(crypto);
const hashWithoutPrivate = await contextWithoutPrivate.hash(crypto);

// The hashes should be different because private attributes are included in the hash
expect(hashWithPrivate).not.toEqual(hashWithoutPrivate);
});

it('uses the keys the keys of attributes in the hash', async () => {
const a = Context.fromLDContext({
kind: 'user',
key: 'testKey',
a: 'b',
});

const b = Context.fromLDContext({
kind: 'user',
key: 'testKey',
b: 'b',
});

const hashA = await a.hash(crypto);
const hashB = await b.hash(crypto);
expect(hashA).not.toBe(hashB);
});

it('uses the keys of nested objects inside the hash', async () => {
const a = Context.fromLDContext({
kind: 'user',
key: 'testKey',
nested: {
level1: {
level2: {
value: 'deep',
},
},
},
});

const b = Context.fromLDContext({
kind: 'user',
key: 'testKey',
nested: {
sub1: {
sub2: {
value: 'deep',
},
},
},
});

const hashA = await a.hash(crypto);
const hashB = await b.hash(crypto);
expect(hashA).not.toBe(hashB);
});

it('it uses the values of nested array in calculations', async () => {
const a = Context.fromLDContext({
kind: 'user',
key: 'testKey',
array: [1, 2, 3],
nestedArray: [
[1, 2],
[3, 4],
],
});

const b = Context.fromLDContext({
kind: 'user',
key: 'testKey',
array: [1, 2, 3],
nestedArray: [
[2, 1],
[3, 4],
],
});

const hashA = await a.hash(crypto);
const hashB = await b.hash(crypto);
expect(hashA).not.toBe(hashB);
});

it('uses the values of nested objects inside the hash', async () => {
const a = Context.fromLDContext({
kind: 'user',
key: 'testKey',
nested: {
level1: {
level2: {
value: 'deep',
},
},
},
});

const b = Context.fromLDContext({
kind: 'user',
key: 'testKey',
nested: {
level1: {
level2: {
value: 'deeper',
},
},
},
});

const hashA = await a.hash(crypto);
const hashB = await b.hash(crypto);
expect(hashA).not.toBe(hashB);
});

it('hashes _meta in attributes', async () => {
const a = Context.fromLDContext({
kind: 'user',
key: 'testKey',
nested: {
level1: {
level2: {
_meta: { test: 'a' },
},
},
},
});

const b = Context.fromLDContext({
kind: 'user',
key: 'testKey',
nested: {
level1: {
level2: {
_meta: { test: 'b' },
},
},
},
});

const hashA = await a.hash(crypto);
const hashB = await b.hash(crypto);
expect(hashA).not.toBe(hashB);
});

it('produces the same value for the given context', async () => {
// This isn't so much a test as it is a detection of change.
// If this test failed, and you didn't expect it, then you probably need to make sure your
// change makes sense.
const complexContext = Context.fromLDContext({
kind: 'multi',
org: {
key: 'testKey',
name: 'testName',
cat: 'calico',
dog: 'lab',
anonymous: true,
nestedArray: [
[1, 2],
[3, 4],
],
_meta: {
privateAttributes: ['/a/b/c', 'cat', 'custom/dog'],
},
},
customer: {
key: 'testKey',
name: 'testName',
bird: 'party parrot',
chicken: 'hen',
nested: {
level1: {
level2: {
value: 'deep',
_meta: { thisShouldBeInTheHash: true },
},
},
},
},
});
expect(await complexContext.hash(crypto)).toBe(
'{"_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}',
);
});

it('collisiontest', async () => {
const a = Context.fromLDContext({
kind: 'user',
key: 'bob',
a: 'bcd',
});

const b = Context.fromLDContext({
kind: 'user',
key: 'bob',
a: { b: { c: 'd' } },
});

const hashA = await a.hash(crypto);
const hashB = await b.hash(crypto);
expect(hashA).not.toBe(hashB);
});
});
Loading