Skip to content

Commit

Permalink
feat: Add migration operation input event and tracker. (#214)
Browse files Browse the repository at this point in the history
  • Loading branch information
kinyoklion authored Aug 1, 2023
1 parent bee4e74 commit 2a1eb6f
Show file tree
Hide file tree
Showing 9 changed files with 449 additions and 30 deletions.
19 changes: 3 additions & 16 deletions packages/shared/sdk-server/__tests__/LDClient.migrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ describe('given an LDClient with test data', () => {
{ key: 'test-key' },
defaultValue as LDMigrationStage,
);
expect(res).toEqual(value);
expect(res.value).toEqual(value);
},
);

Expand All @@ -76,30 +76,17 @@ describe('given an LDClient with test data', () => {
])('returns the default value if the flag does not exist: default = %p', async (stage) => {
const res = await client.variationMigration('no-flag', { key: 'test-key' }, stage);

expect(res).toEqual(stage);
expect(res.value).toEqual(stage);
});

it('produces an error event for a migration flag with an incorrect value', async () => {
const flagKey = 'bad-migration';
td.update(td.flag(flagKey).valueForAll('potato'));
const res = await client.variationMigration(flagKey, { key: 'test-key' }, LDMigrationStage.Off);
expect(res).toEqual(LDMigrationStage.Off);
expect(res.value).toEqual(LDMigrationStage.Off);
expect(errors.length).toEqual(1);
expect(errors[0].message).toEqual(
'Unrecognized MigrationState for "bad-migration"; returning default value.',
);
});

it('includes an error in the node callback', (done) => {
const flagKey = 'bad-migration';
td.update(td.flag(flagKey).valueForAll('potato'));
client.variationMigration(flagKey, { key: 'test-key' }, LDMigrationStage.Off, (err, value) => {
const error = err as Error;
expect(error.message).toEqual(
'Unrecognized MigrationState for "bad-migration"; returning default value.',
);
expect(value).toEqual(LDMigrationStage.Off);
done();
});
});
});
185 changes: 185 additions & 0 deletions packages/shared/sdk-server/__tests__/MigrationOpTracker.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { Context } from '@launchdarkly/js-sdk-common';

import { LDConsistencyCheck, LDMigrationStage } from '../src';
import MigrationOpTracker from '../src/MigrationOpTracker';

it('does not generate an event if an op is not set', () => {
const tracker = new MigrationOpTracker(
'flag',
Context.fromLDContext({ kind: 'user', key: 'bob' }),
LDMigrationStage.Off,
LDMigrationStage.Off,
{
kind: 'FALLTHROUGH',
},
);

expect(tracker.createEvent()).toBeUndefined();
});

it('does not generate an event for an invalid context', () => {
const tracker = new MigrationOpTracker(
'flag',
Context.fromLDContext({ kind: 'kind', key: '' }),
LDMigrationStage.Off,
LDMigrationStage.Off,
{
kind: 'FALLTHROUGH',
},
);

// Set the op otherwise that would prevent an event as well.
tracker.op('write');

expect(tracker.createEvent()).toBeUndefined();
});

it('generates an event if the minimal requirements are met.', () => {
const tracker = new MigrationOpTracker(
'flag',
Context.fromLDContext({ kind: 'user', key: 'bob' }),
LDMigrationStage.Off,
LDMigrationStage.Off,
{
kind: 'FALLTHROUGH',
},
);

tracker.op('write');

expect(tracker.createEvent()).toMatchObject({
contextKeys: { user: 'bob' },
evaluation: { default: 'off', key: 'flag', reason: { kind: 'FALLTHROUGH' }, value: 'off' },
kind: 'migration_op',
measurements: [],
operation: 'write',
});
});

it('includes errors if at least one is set', () => {
const tracker = new MigrationOpTracker(
'flag',
Context.fromLDContext({ kind: 'user', key: 'bob' }),
LDMigrationStage.Off,
LDMigrationStage.Off,
{
kind: 'FALLTHROUGH',
},
);
tracker.op('read');
tracker.error('old');

const event = tracker.createEvent();
expect(event?.measurements).toContainEqual({
key: 'error',
values: {
old: 1,
new: 0,
},
});

const trackerB = new MigrationOpTracker(
'flag',
Context.fromLDContext({ kind: 'user', key: 'bob' }),
LDMigrationStage.Off,
LDMigrationStage.Off,
{
kind: 'FALLTHROUGH',
},
);
trackerB.op('read');
trackerB.error('new');

const eventB = trackerB.createEvent();
expect(eventB?.measurements).toContainEqual({
key: 'error',
values: {
old: 0,
new: 1,
},
});
});

it('includes latency if at least one measurement exists', () => {
const tracker = new MigrationOpTracker(
'flag',
Context.fromLDContext({ kind: 'user', key: 'bob' }),
LDMigrationStage.Off,
LDMigrationStage.Off,
{
kind: 'FALLTHROUGH',
},
);
tracker.op('read');
tracker.latency('old', 100);

const event = tracker.createEvent();
expect(event?.measurements).toContainEqual({
key: 'latency',
values: {
old: 100,
},
});

const trackerB = new MigrationOpTracker(
'flag',
Context.fromLDContext({ kind: 'user', key: 'bob' }),
LDMigrationStage.Off,
LDMigrationStage.Off,
{
kind: 'FALLTHROUGH',
},
);
trackerB.op('read');
trackerB.latency('new', 150);

const eventB = trackerB.createEvent();
expect(eventB?.measurements).toContainEqual({
key: 'latency',
values: {
new: 150,
},
});
});

it('includes if the result was consistent', () => {
const tracker = new MigrationOpTracker(
'flag',
Context.fromLDContext({ kind: 'user', key: 'bob' }),
LDMigrationStage.Off,
LDMigrationStage.Off,
{
kind: 'FALLTHROUGH',
},
);
tracker.op('read');
tracker.consistency(LDConsistencyCheck.Consistent);

const event = tracker.createEvent();
expect(event?.measurements).toContainEqual({
key: 'consistent',
value: 1,
samplingOdds: 0,
});
});

it('includes if the result was inconsistent', () => {
const tracker = new MigrationOpTracker(
'flag',
Context.fromLDContext({ kind: 'user', key: 'bob' }),
LDMigrationStage.Off,
LDMigrationStage.Off,
{
kind: 'FALLTHROUGH',
},
);
tracker.op('read');
tracker.consistency(LDConsistencyCheck.Inconsistent);

const event = tracker.createEvent();
expect(event?.measurements).toContainEqual({
key: 'consistent',
value: 0,
samplingOdds: 0,
});
});
36 changes: 28 additions & 8 deletions packages/shared/sdk-server/src/LDClientImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
LDClient,
LDFlagsState,
LDFlagsStateOptions,
LDMigrationDetail,
LDMigrationStage,
LDOptions,
LDStreamProcessor,
Expand Down Expand Up @@ -45,6 +46,7 @@ import EventSender from './events/EventSender';
import isExperiment from './events/isExperiment';
import NullEventProcessor from './events/NullEventProcessor';
import FlagsStateBuilder from './FlagsStateBuilder';
import MigrationOpTracker from './MigrationOpTracker';
import Configuration from './options/Configuration';
import AsyncStoreFacade from './store/AsyncStoreFacade';
import VersionedDataKinds from './store/VersionedDataKinds';
Expand Down Expand Up @@ -274,17 +276,35 @@ export default class LDClientImpl implements LDClient {
key: string,
context: LDContext,
defaultValue: LDMigrationStage,
callback?: (err: any, res: LDMigrationStage) => void,
): Promise<LDMigrationStage> {
const stringValue = await this.variation(key, context, defaultValue as string);
if (!IsMigrationStage(stringValue)) {
): Promise<LDMigrationDetail> {
const convertedContext = Context.fromLDContext(context);
const detail = await this.variationDetail(key, context, defaultValue as string);
if (!IsMigrationStage(detail.value)) {
const error = new Error(`Unrecognized MigrationState for "${key}"; returning default value.`);
this.onError(error);
callback?.(error, defaultValue);
return defaultValue;
const reason = {
kind: 'ERROR',
errorKind: 'WRONG_TYPE',
};
return {
value: defaultValue,
reason,
tracker: new MigrationOpTracker(key, convertedContext, defaultValue, defaultValue, reason),
};
}
callback?.(null, stringValue as LDMigrationStage);
return stringValue as LDMigrationStage;
return {
...detail,
value: detail.value as LDMigrationStage,
tracker: new MigrationOpTracker(
key,
convertedContext,
defaultValue,
defaultValue,
detail.reason,
// Can be null for compatibility reasons.
detail.variationIndex === null ? undefined : detail.variationIndex,
),
};
}

async allFlagsState(
Expand Down
4 changes: 2 additions & 2 deletions packages/shared/sdk-server/src/Migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ export default class Migration<
payload?: TMigrationReadInput,
): Promise<LDMigrationReadResult<TMigrationRead>> {
const stage = await this.client.variationMigration(key, context, defaultStage);
return this.readTable[stage](this.config, payload);
return this.readTable[stage.value](this.config, payload);
}

async write(
Expand All @@ -296,6 +296,6 @@ export default class Migration<
): Promise<LDMigrationWriteResult<TMigrationWrite>> {
const stage = await this.client.variationMigration(key, context, defaultStage);

return this.writeTable[stage](this.config, payload);
return this.writeTable[stage.value](this.config, payload);
}
}
Loading

0 comments on commit 2a1eb6f

Please sign in to comment.