Skip to content
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

feat: edge sdks should send events to bulk/environment endpoint #256

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a2dbfe4
feat: implement useEnvironmentEndpoint logic by inspecting the sdk-ke…
Aug 26, 2023
cada2f6
fix: broken tests.
Aug 26, 2023
37bdbf4
fix: wrong events path for clientSideID
Aug 26, 2023
cfbc545
fix: added shell directive. added link-dev for cloudflare/example to …
Aug 28, 2023
2874194
chore: remove debug statements.
Aug 28, 2023
8bcd58a
Merge branch 'main' into yus/sc-214758/send-events-to-environment-end…
yusinto Aug 28, 2023
1908227
chore: better support for event sender analytics paths (#259)
yusinto Aug 28, 2023
11cb96a
chore: rollback unit test sdk- prefix changes.
yusinto Aug 28, 2023
a3d3132
chor: add comments
yusinto Aug 28, 2023
429e92a
Update packages/shared/sdk-server-edge/src/api/LDClient.ts
yusinto Aug 28, 2023
4741775
Merge branch 'main' into yus/sc-214758/send-events-to-environment-end…
Aug 29, 2023
f871209
chore: add release-please alpha config.
Aug 29, 2023
37ebde7
Update release-please-config.json
Aug 29, 2023
ad7d012
chore: flush events in waitUntil. improved handler types.
yusinto Aug 30, 2023
303e3ae
Merge branch 'main' into yus/sc-214758/send-events-to-environment-end…
yusinto Sep 21, 2023
44c8044
fix: remove link-dev and use yarn workspaces.
yusinto Sep 22, 2023
2abeb13
fix: unit tests due to missing ctx. update jest version.
yusinto Sep 22, 2023
8f1ad45
chore: fix flush callback arg types.
yusinto Sep 22, 2023
8b5c0d9
fix: close client to fix jest open handle warnings
yusinto Sep 22, 2023
6cb2165
Merge branch 'main' into yus/sc-214758/send-events-to-environment-end…
yusinto Nov 7, 2023
aae16d8
chore: resolve conflicts.
yusinto Nov 7, 2023
2acad70
chore: in cloudflare example, rename sdkKey to clientSideID.
yusinto Nov 7, 2023
5da020e
chore: added unit tests for edge sdk.
yusinto Nov 8, 2023
474cdf7
chore: update cloudflare sdk dep version
yusinto Nov 8, 2023
55bf9af
chore: set test data trackEvents to true
yusinto Nov 8, 2023
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
9 changes: 5 additions & 4 deletions packages/sdk/cloudflare/example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
"module": "./dist/index.mjs",
"packageManager": "yarn@3.4.1",
"dependencies": {
"@launchdarkly/cloudflare-server-sdk": "^2.1.4"
"@launchdarkly/cloudflare-server-sdk": "2.2.3"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20230321.0",
"@types/jest": "^27.5.1",
"@types/jest": "^29.5.5",
"esbuild": "^0.14.41",
"jest": "^28.1.0",
"jest": "^29.7.0",
"jest-environment-miniflare": "^2.5.0",
"miniflare": "^2.5.0",
"prettier": "^2.6.2",
Expand All @@ -23,6 +23,7 @@
"build": "node build.js",
"start": "wrangler dev",
"deploy": "wrangler publish",
"test": "yarn build && jest"
"test": "yarn build && jest",
"clean": "rm -rf dist && rm -rf node_modules && rm -rf .yarn/cache && yarn build"
}
}
25 changes: 23 additions & 2 deletions packages/sdk/cloudflare/example/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,41 @@ import testData from './testData.json';

describe('test', () => {
let env: Bindings;
let mockExecutionContext: ExecutionContext;

beforeEach(async () => {
// solves jest complaining about console.log in flush after exiting
// eslint-disable-next-line no-console
console.log = jest.fn();

mockExecutionContext = {
waitUntil: jest.fn(),
passThroughOnException: jest.fn(),
};
env = getMiniflareBindings();
const { LD_KV } = env;
await LD_KV.put('LD-Env-test-sdk-key', JSON.stringify(testData));
});

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

test('variation true', async () => {
const res = await app.fetch(new Request('http://localhost/?email=truemail'), env);
const res = await app.fetch(
new Request('http://localhost/?email=truemail'),
env,
mockExecutionContext,
);
expect(await res.text()).toContain('testFlag1: true');
});

test('variation false', async () => {
const res = await app.fetch(new Request('http://localhost/?email=falsemail'), env);
const res = await app.fetch(
new Request('http://localhost/?email=falsemail'),
env,
mockExecutionContext,
);
expect(await res.text()).toContain('testFlag1: false');
});
});
19 changes: 15 additions & 4 deletions packages/sdk/cloudflare/example/src/index.ts
Copy link
Contributor

Choose a reason for hiding this comment

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

To the Vercel and/or Akamai examples also need to be updated? I initially thought this change only applied to Cloudflare because only the Cloudflare example was changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Those examples can be updated, but not necessary. I'll leave that to @ldhenry .

Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/* eslint-disable no-console */
import { init as initLD } from '@launchdarkly/cloudflare-server-sdk';

export default {
async fetch(request: Request, env: Bindings): Promise<Response> {
const sdkKey = 'test-sdk-key';
async fetch(request: Request, env: Bindings, ctx: ExecutionContext): Promise<Response> {
const clientSideID = 'test-client-side-id';
const flagKey = 'testFlag1';
const { searchParams } = new URL(request.url);

Expand All @@ -11,7 +12,7 @@ export default {
const context = { kind: 'user', key: 'test-user-key-1', email };

// start using ld
const client = initLD(sdkKey, env.LD_KV);
const client = initLD(clientSideID, env.LD_KV, { sendEvents: true });
await client.waitForInitialization();
const flagValue = await client.variation(flagKey, context, false);
const flagDetail = await client.variationDetail(flagKey, context, false);
Expand All @@ -22,8 +23,18 @@ export default {
detail: ${JSON.stringify(flagDetail)}
allFlags: ${JSON.stringify(allFlags)}`;

// eslint-disable-next-line
console.log(`------------- ${resp}`);

// Gotcha: you must call flush otherwise events will not be sent to LD servers
// due to the ephemeral nature of edge workers.
// https://developers.cloudflare.com/workers/runtime-apis/fetch-event/#waituntil
ctx.waitUntil(
client.flush((err: Error | null, res: boolean) => {
console.log(`flushed events result: ${res}, error: ${err}`);
client.close();
}),
);

return new Response(`${resp}`);
},
};
10 changes: 5 additions & 5 deletions packages/sdk/cloudflare/example/src/testData.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"negate": false
}
],
"trackEvents": false,
"trackEvents": true,
"rollout": {
"bucketBy": "bucket",
"variations": [{ "variation": 1, "weight": 100 }]
Expand All @@ -36,7 +36,7 @@
},
"clientSide": true,
"salt": "aef830243d6640d0a973be89988e008d",
"trackEvents": false,
"trackEvents": true,
"trackEventsFallthrough": false,
"debugEventsUntilDate": null,
"version": 2,
Expand Down Expand Up @@ -65,7 +65,7 @@
},
"clientSide": true,
"salt": "aef830243d6640d0a973be89988e008d",
"trackEvents": false,
"trackEvents": true,
"trackEventsFallthrough": false,
"debugEventsUntilDate": null,
"version": 2,
Expand All @@ -87,7 +87,7 @@
"negate": false
}
],
"trackEvents": false
"trackEvents": true
}
],
"fallthrough": {
Expand All @@ -101,7 +101,7 @@
},
"clientSide": true,
"salt": "aef830243d6640d0a973be89988e008d",
"trackEvents": false,
"trackEvents": true,
"trackEventsFallthrough": false,
"debugEventsUntilDate": null,
"version": 2,
Expand Down
12 changes: 6 additions & 6 deletions packages/sdk/cloudflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,20 @@ export type { LDClient };
* (`new LDClient()/new LDClientImpl()/new LDClient()`); the SDK does not currently support
* this.
*
* @param clientSideID
* The client side ID. This is only used to query the kvNamespace above,
* not to connect with LaunchDarkly servers.
* @param kvNamespace
* The Cloudflare KV configured for LaunchDarkly.
* @param sdkKey
* The client side SDK key. This is only used to query the kvNamespace above,
* not to connect with LaunchDarkly servers.
* @param options
* Optional configuration settings. The only supported option is logger.
* @return
* The new {@link LDClient} instance.
*/
export const init = (sdkKey: string, kvNamespace: KVNamespace, options: LDOptions = {}) => {
export const init = (clientSideID: string, kvNamespace: KVNamespace, options: LDOptions = {}) => {
const logger = options.logger ?? BasicLogger.get();
return initEdge(sdkKey, createPlatformInfo(), {
featureStore: new EdgeFeatureStore(kvNamespace, sdkKey, 'Cloudflare', logger),
return initEdge(clientSideID, createPlatformInfo(), {
featureStore: new EdgeFeatureStore(kvNamespace, clientSideID, 'Cloudflare', logger),
logger,
...options,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@ jest.mock('../../utils', () => {

const basicConfig = {
tags: new ApplicationTags({ application: { id: 'testApplication1', version: '1.0.0' } }),
serviceEndpoints: { events: 'https://events.fake.com', streaming: '', polling: '' },
serviceEndpoints: {
events: 'https://events.fake.com',
streaming: '',
polling: '',
analyticsEventPath: '/bulk',
diagnosticEventPath: '/diagnostic',
includeAuthorizationHeader: true,
},
};
const testEventData1 = { eventId: 'test-event-data-1' };
const testEventData2 = { eventId: 'test-event-data-2' };
Expand Down
17 changes: 13 additions & 4 deletions packages/shared/common/src/internal/events/EventSender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,21 @@ export default class EventSender implements LDEventSender {

constructor(clientContext: ClientContext) {
const { basicConfiguration, platform } = clientContext;
const { sdkKey, serviceEndpoints, tags } = basicConfiguration;
const {
sdkKey,
serviceEndpoints: {
events,
analyticsEventPath,
diagnosticEventPath,
includeAuthorizationHeader,
},
tags,
} = basicConfiguration;
const { crypto, info, requests } = platform;

this.defaultHeaders = defaultHeaders(sdkKey, info, tags);
this.eventsUri = `${serviceEndpoints.events}/bulk`;
this.diagnosticEventsUri = `${serviceEndpoints.events}/diagnostic`;
this.defaultHeaders = defaultHeaders(sdkKey, info, tags, includeAuthorizationHeader);
this.eventsUri = `${events}${analyticsEventPath}`;
this.diagnosticEventsUri = `${events}${diagnosticEventPath}`;
this.requests = requests;
this.crypto = crypto;
}
Expand Down
15 changes: 15 additions & 0 deletions packages/shared/common/src/internal/events/LDInternalOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* This is for internal use only.
*
* Edge sdks use clientSideID to query feature stores. They also send analytics
* using this clientSideID. This is a hybrid behavior because they are based
* on js-server-common, but uses the clientSideID instead of the sdkKey for the
* above reasons. These internal options allow the edge sdks to use the
* EventSender to send analytics to the correct LD endpoints using
* the clientSideId.
*/
export type LDInternalOptions = {
analyticsEventPath?: string;
diagnosticEventPath?: string;
includeAuthorizationHeader?: boolean;
};
2 changes: 2 additions & 0 deletions packages/shared/common/src/internal/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import InputEvalEvent from './InputEvalEvent';
import InputEvent from './InputEvent';
import InputIdentifyEvent from './InputIdentifyEvent';
import InputMigrationEvent from './InputMigrationEvent';
import type { LDInternalOptions } from './LDInternalOptions';
import NullEventProcessor from './NullEventProcessor';
import shouldSample from './sampling';

Expand All @@ -18,4 +19,5 @@ export {
EventProcessor,
shouldSample,
NullEventProcessor,
LDInternalOptions,
};
24 changes: 24 additions & 0 deletions packages/shared/common/src/options/ServiceEndpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,37 @@ export default class ServiceEndpoints {
public readonly streaming: string;
public readonly polling: string;
public readonly events: string;

/** Valid paths are:
* /bulk
* /events/bulk/envId
* /mobile
*/
public readonly analyticsEventPath: string;

/** Valid paths are:
* /diagnostic
* /events/diagnostic/envId
* /mobile/events/diagnostic
*/
public readonly diagnosticEventPath: string;

// if true the sdk key will be included as authorization header
public readonly includeAuthorizationHeader: boolean;

public constructor(
streaming: string,
polling: string,
events: string = ServiceEndpoints.DEFAULT_EVENTS,
analyticsEventPath: string = '/bulk',
diagnosticEventPath: string = '/diagnostic',
includeAuthorizationHeader: boolean = true,
) {
this.streaming = canonicalizeUri(streaming);
this.polling = canonicalizeUri(polling);
this.events = canonicalizeUri(events);
this.analyticsEventPath = analyticsEventPath;
this.diagnosticEventPath = diagnosticEventPath;
this.includeAuthorizationHeader = includeAuthorizationHeader;
}
}
16 changes: 13 additions & 3 deletions packages/shared/common/src/utils/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,30 @@ import { Info } from '../api';
import { ApplicationTags } from '../options';

export type LDHeaders = {
authorization: string;
authorization?: string;
'user-agent': string;
'x-launchdarkly-wrapper'?: string;
'x-launchdarkly-tags'?: string;
};

export function defaultHeaders(sdkKey: string, info: Info, tags?: ApplicationTags): LDHeaders {
export function defaultHeaders(
sdkKey: string,
info: Info,
tags?: ApplicationTags,
includeAuthorizationHeader: boolean = true,
): LDHeaders {
const { userAgentBase, version, wrapperName, wrapperVersion } = info.sdkData();

const headers: LDHeaders = {
authorization: sdkKey,
'user-agent': `${userAgentBase ?? 'NodeJSClient'}/${version}`,
};

// edge sdks sets this to false because they use the clientSideID
// and they don't need the authorization header
if (includeAuthorizationHeader) {
headers.authorization = sdkKey;
}

if (wrapperName) {
headers['x-launchdarkly-wrapper'] = wrapperVersion
? `${wrapperName}/${wrapperVersion}`
Expand Down
9 changes: 8 additions & 1 deletion packages/shared/mocks/src/clientContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ import basicPlatform from './platform';
const clientContext: ClientContext = {
basicConfiguration: {
sdkKey: 'testSdkKey',
serviceEndpoints: { events: '', polling: '', streaming: 'https://mockstream.ld.com' },
serviceEndpoints: {
events: '',
polling: '',
streaming: 'https://mockstream.ld.com',
diagnosticEventPath: '/diagnostic',
analyticsEventPath: '/bulk',
includeAuthorizationHeader: true,
},
},
platform: basicPlatform,
};
Expand Down
1 change: 1 addition & 0 deletions packages/shared/sdk-server-edge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"crypto-js": "^4.1.1"
},
"devDependencies": {
"@launchdarkly/private-js-mocks": "0.0.1",
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
"@types/crypto-js": "^4.1.1",
"@types/jest": "^29.5.0",
Expand Down
41 changes: 41 additions & 0 deletions packages/shared/sdk-server-edge/src/api/LDClient.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { internal } from '@launchdarkly/js-server-sdk-common';
import { basicPlatform } from '@launchdarkly/private-js-mocks';

import LDClient from './LDClient';

jest.mock('@launchdarkly/js-sdk-common', () => {
const actual = jest.requireActual('@launchdarkly/js-sdk-common');
return {
...actual,
...{
internal: {
...actual.internal,
DiagnosticsManager: jest.fn(),
EventProcessor: jest.fn(),
},
},
};
});

const mockEventProcessor = internal.EventProcessor as jest.Mock;
describe('Edge LDClient', () => {
it('uses clientSideID endpoints', async () => {
const client = new LDClient('client-side-id', basicPlatform.info, {
sendEvents: true,
});
await client.waitForInitialization();
const passedConfig = mockEventProcessor.mock.calls[0][0];

expect(passedConfig).toMatchObject({
sendEvents: true,
serviceEndpoints: {
includeAuthorizationHeader: false,
analyticsEventPath: '/events/bulk/client-side-id',
diagnosticEventPath: '/events/diagnostic/client-side-id',
events: 'https://events.launchdarkly.com',
polling: 'https://sdk.launchdarkly.com',
streaming: 'https://stream.launchdarkly.com',
},
});
});
});
Loading