Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion packages/shared/common/src/internal/events/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ClientMessages from './ClientMessages';
import EventProcessor from './EventProcessor';
import EventProcessor, { EventProcessorOptions } from './EventProcessor';
import InputCustomEvent from './InputCustomEvent';
import InputEvalEvent from './InputEvalEvent';
import InputEvent from './InputEvent';
Expand All @@ -17,6 +17,7 @@ export {
InputIdentifyEvent,
InputMigrationEvent,
EventProcessor,
EventProcessorOptions,
shouldSample,
NullEventProcessor,
LDInternalOptions,
Expand Down
19 changes: 19 additions & 0 deletions packages/shared/mocks/src/eventProcessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { ClientContext, internal, subsystem } from '@common';

export const MockEventProcessor = jest.fn();

export const setupMockEventProcessor = () => {
MockEventProcessor.mockImplementation(
(
_config: internal.EventProcessorOptions,
_clientContext: ClientContext,
_contextDeduplicator?: subsystem.LDContextDeduplicator,
_diagnosticsManager?: internal.DiagnosticsManager,
_start: boolean = true,
) => ({
close: jest.fn(),
flush: jest.fn(),
sendEvent: jest.fn(),
}),
);
};
3 changes: 3 additions & 0 deletions packages/shared/mocks/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { clientContext } from './clientContext';
import ContextDeduplicator from './contextDeduplicator';
import { hasher } from './crypto';
import { MockEventProcessor, setupMockEventProcessor } from './eventProcessor';
import logger from './logger';
import mockFetch from './mockFetch';
import { basicPlatform } from './platform';
Expand All @@ -13,6 +14,8 @@ export {
mockFetch,
logger,
ContextDeduplicator,
MockEventProcessor,
setupMockEventProcessor,
MockStreamingProcessor,
setupMockStreamingProcessor,
};
76 changes: 76 additions & 0 deletions packages/shared/sdk-client/src/LDClientImpl.events.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { AutoEnvAttributes, clone, LDContext } from '@launchdarkly/js-sdk-common';
import { InputIdentifyEvent } from '@launchdarkly/js-sdk-common/dist/internal';
import {
basicPlatform,
hasher,
logger,
MockEventProcessor,
setupMockEventProcessor,
setupMockStreamingProcessor,
} from '@launchdarkly/private-js-mocks';

import * as mockResponseJson from './evaluation/mockResponse.json';
import LDClientImpl from './LDClientImpl';
import { Flags } from './types';

jest.mock('@launchdarkly/js-sdk-common', () => {
const actual = jest.requireActual('@launchdarkly/js-sdk-common');
const m = jest.requireActual('@launchdarkly/private-js-mocks');
return {
...actual,
...{
internal: {
...actual.internal,
StreamingProcessor: m.MockStreamingProcessor,
EventProcessor: m.MockEventProcessor,
},
},
};
});

const testSdkKey = 'test-sdk-key';
let ldc: LDClientImpl;
let defaultPutResponse: Flags;

describe('sdk-client object', () => {
beforeEach(() => {
defaultPutResponse = clone<Flags>(mockResponseJson);
setupMockEventProcessor();
setupMockStreamingProcessor(false, defaultPutResponse);
basicPlatform.crypto.randomUUID.mockReturnValue('random1');
hasher.digest.mockReturnValue('digested1');

ldc = new LDClientImpl(testSdkKey, AutoEnvAttributes.Enabled, basicPlatform, {
logger,
});
jest
.spyOn(LDClientImpl.prototype as any, 'createStreamUriPath')
.mockReturnValue('/stream/path');
});

afterEach(() => {
jest.resetAllMocks();
});

test('identify event', async () => {
defaultPutResponse['dev-test-flag'].value = false;
const carContext: LDContext = { kind: 'car', key: 'test-car' };

await ldc.identify(carContext);

expect(MockEventProcessor).toHaveBeenCalled();
expect(ldc.eventProcessor!.sendEvent).toHaveBeenNthCalledWith(
1,
expect.objectContaining<InputIdentifyEvent>({
kind: 'identify',
context: expect.objectContaining({
contexts: expect.objectContaining({
car: { key: 'test-car' },
}),
}),
creationDate: expect.any(Number),
samplingRatio: expect.any(Number),
}),
);
});
});
33 changes: 30 additions & 3 deletions packages/shared/sdk-client/src/LDClientImpl.storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,32 @@ describe('sdk-client storage', () => {
});
});

test('not emitting change event', async () => {
jest.doMock('./utils', () => {
const actual = jest.requireActual('./utils');
return {
...actual,
calculateFlagChanges: () => [],
};
});
let LDClientImplTestNoChange;
jest.isolateModules(async () => {
LDClientImplTestNoChange = jest.requireActual('./LDClientImpl').default;
ldc = new LDClientImplTestNoChange(testSdkKey, AutoEnvAttributes.Enabled, basicPlatform, {
logger,
sendEvents: false,
});
});

// @ts-ignore
emitter = ldc.emitter;
jest.spyOn(emitter as LDEmitter, 'emit');

await identifyGetAllFlags(true, defaultPutResponse);

expect(emitter.emit).not.toHaveBeenCalled();
});

test('no storage, cold start from streamer', async () => {
// fake previously cached flags even though there's no storage for this context
// @ts-ignore
Expand All @@ -174,7 +200,9 @@ describe('sdk-client storage', () => {
'org:Testy Pizza',
JSON.stringify(defaultPutResponse),
);
expect(ldc.logger.debug).toHaveBeenCalledWith('Not emitting changes from PUT');
expect(ldc.logger.debug).toHaveBeenCalledWith(
'OnIdentifyResolve no changes to emit from: streamer PUT.',
);

// this is defaultPutResponse
expect(ldc.allFlags()).toEqual({
Expand Down Expand Up @@ -205,14 +233,13 @@ describe('sdk-client storage', () => {

test('syncing storage when a flag is added', async () => {
const putResponse = clone<Flags>(defaultPutResponse);
const newFlag = {
putResponse['another-dev-test-flag'] = {
version: 1,
flagVersion: 2,
value: false,
variation: 1,
trackEvents: false,
};
putResponse['another-dev-test-flag'] = newFlag;
const allFlags = await identifyGetAllFlags(false, putResponse);

expect(allFlags).toMatchObject({ 'another-dev-test-flag': false });
Expand Down
53 changes: 32 additions & 21 deletions packages/shared/sdk-client/src/LDClientImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,20 +168,8 @@ export default class LDClientImpl implements LDClient {
deserializeData: JSON.parse,
processJson: async (dataJson: Flags) => {
this.logger.debug(`Streamer PUT: ${Object.keys(dataJson)}`);
const changedKeys = calculateFlagChanges(this.flags, dataJson);
this.context = context;
this.flags = dataJson;
this.onIdentifyResolve(identifyResolve, dataJson, context, 'streamer PUT');
await this.platform.storage?.set(canonicalKey, JSON.stringify(this.flags));

if (changedKeys.length > 0) {
this.logger.debug(`Emitting changes from PUT: ${changedKeys}`);
// emitting change resolves identify
this.emitter.emit('change', context, changedKeys);
} else {
// manually resolve identify
this.logger.debug('Not emitting changes from PUT');
identifyResolve();
}
Comment on lines -171 to -184
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Dry'd this to onIdentifyResolve.

},
});

Expand Down Expand Up @@ -249,7 +237,7 @@ export default class LDClientImpl implements LDClient {
);
}

private createPromiseWithListeners() {
private createIdentifyPromise() {
let res: any;
const p = new Promise<void>((resolve, reject) => {
res = resolve;
Expand All @@ -263,7 +251,6 @@ export default class LDClientImpl implements LDClient {

this.identifyChangeListener = (c: LDContext, changedKeys: string[]) => {
this.logger.debug(`change: context: ${JSON.stringify(c)}, flags: ${changedKeys}`);
resolve();
Copy link
Contributor Author

@yusinto yusinto Mar 25, 2024

Choose a reason for hiding this comment

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

Resolve is now explicitly called when appropriate, rather than here. This is more sensible because not all 'change' events need to resolve the identify promise.

};
this.identifyErrorListener = (c: LDContext, err: any) => {
this.logger.debug(`error: ${err}, context: ${JSON.stringify(c)}`);
Expand Down Expand Up @@ -297,17 +284,14 @@ export default class LDClientImpl implements LDClient {
return Promise.reject(error);
}

const { identifyPromise, identifyResolve } = this.createPromiseWithListeners();
this.eventProcessor?.sendEvent(this.eventFactoryDefault.identifyEvent(checkedContext));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was the missing piece to send identify event.

const { identifyPromise, identifyResolve } = this.createIdentifyPromise();
this.logger.debug(`Identifying ${JSON.stringify(context)}`);

const flagsStorage = await this.getFlagsFromStorage(checkedContext.canonicalKey);
if (flagsStorage) {
this.logger.debug('Using storage');

const changedKeys = calculateFlagChanges(this.flags, flagsStorage);
this.context = context;
this.flags = flagsStorage;
this.emitter.emit('change', context, changedKeys);
this.onIdentifyResolve(identifyResolve, flagsStorage, context, 'identify storage');
}

if (this.isOffline()) {
Expand Down Expand Up @@ -342,6 +326,33 @@ export default class LDClientImpl implements LDClient {
return identifyPromise;
}

/**
* Performs common tasks when resolving the identify promise:
* - resolve the promise
* - update in memory context
* - update in memory flags
* - emit change event if needed
*
* @param resolve
* @param flags
* @param context
* @param source For logging purposes
* @private
*/
private onIdentifyResolve(resolve: any, flags: Flags, context: LDContext, source: string) {
resolve();
const changedKeys = calculateFlagChanges(this.flags, flags);
this.context = context;
this.flags = flags;

if (changedKeys.length > 0) {
this.emitter.emit('change', context, changedKeys);
this.logger.debug(`OnIdentifyResolve emitting changes from: ${source}.`);
} else {
this.logger.debug(`OnIdentifyResolve no changes to emit from: ${source}.`);
}
}

off(eventName: EventName, listener: Function): void {
this.emitter.off(eventName, listener);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { ClientContext, internal, Platform } from '@launchdarkly/js-sdk-common';
import { EventProcessor } from '@launchdarkly/js-sdk-common/dist/internal';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is an unrelated bug. I'm not sure why this wasn't caught by the linter.


import Configuration from '../configuration';

Expand All @@ -9,7 +8,7 @@ const createEventProcessor = (
platform: Platform,
diagnosticsManager?: internal.DiagnosticsManager,
start: boolean = false,
): EventProcessor | undefined => {
): internal.EventProcessor | undefined => {
if (config.sendEvents) {
return new internal.EventProcessor(
{ ...config, eventsCapacity: config.capacity },
Expand Down