Skip to content

Commit dc2ad25

Browse files
committed
chore: adding custom event when evaluating from RSC
1 parent fe8bd37 commit dc2ad25

7 files changed

Lines changed: 165 additions & 75 deletions

File tree

packages/sdk/react/__tests__/server/createLDServerSession.test.ts

Lines changed: 15 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,26 @@
1-
import { LDContext, LDFlagsStateOptions } from '@launchdarkly/js-server-sdk-common';
1+
import { LDContext } from '@launchdarkly/js-server-sdk-common';
22

33
import { createLDServerSession } from '../../src/server/index';
4+
import { makeMockServerClient } from './mockServerClient';
45

56
const context: LDContext = { kind: 'user', key: 'test-user' };
67

7-
function makeMockBaseClient() {
8-
return {
9-
initialized: jest.fn(() => true),
10-
boolVariation: jest.fn((_key: string, _ctx: LDContext, def: boolean) => Promise.resolve(def)),
11-
numberVariation: jest.fn((_key: string, _ctx: LDContext, def: number) => Promise.resolve(def)),
12-
stringVariation: jest.fn((_key: string, _ctx: LDContext, def: string) => Promise.resolve(def)),
13-
jsonVariation: jest.fn((_key: string, _ctx: LDContext, def: unknown) => Promise.resolve(def)),
14-
boolVariationDetail: jest.fn((_key: string, _ctx: LDContext, def: boolean) =>
15-
Promise.resolve({ value: def, variationIndex: null, reason: { kind: 'OFF' as const } }),
16-
),
17-
numberVariationDetail: jest.fn((_key: string, _ctx: LDContext, def: number) =>
18-
Promise.resolve({ value: def, variationIndex: null, reason: { kind: 'OFF' as const } }),
19-
),
20-
stringVariationDetail: jest.fn((_key: string, _ctx: LDContext, def: string) =>
21-
Promise.resolve({ value: def, variationIndex: null, reason: { kind: 'OFF' as const } }),
22-
),
23-
jsonVariationDetail: jest.fn((_key: string, _ctx: LDContext, def: unknown) =>
24-
Promise.resolve({ value: def, variationIndex: null, reason: { kind: 'OFF' as const } }),
25-
),
26-
// @ts-ignore — mock return shape matches LDFlagsState structurally
27-
allFlagsState: jest.fn((_context: LDContext, _options?: LDFlagsStateOptions) =>
28-
Promise.resolve({
29-
valid: true,
30-
getFlagValue: jest.fn(),
31-
getFlagReason: jest.fn(),
32-
allValues: jest.fn(() => ({})),
33-
toJSON: jest.fn(() => ({ $flagsState: {}, $valid: true })),
34-
}),
35-
),
36-
};
37-
}
38-
398
it('getContext() returns the context passed at creation', () => {
40-
const client = makeMockBaseClient();
9+
const client = makeMockServerClient();
4110
const session = createLDServerSession(client, context);
4211
expect(session.getContext()).toEqual(context);
4312
});
4413

4514
it('initialized() delegates to the base client', () => {
46-
const client = makeMockBaseClient();
15+
const client = makeMockServerClient();
4716
client.initialized.mockReturnValue(false);
4817
const session = createLDServerSession(client, context);
4918
expect(session.initialized()).toBe(false);
5019
expect(client.initialized).toHaveBeenCalledTimes(1);
5120
});
5221

5322
it('boolVariation() calls base client with bound context', async () => {
54-
const client = makeMockBaseClient();
23+
const client = makeMockServerClient();
5524
client.boolVariation.mockResolvedValue(true);
5625
const session = createLDServerSession(client, context);
5726
const result = await session.boolVariation('my-flag', false);
@@ -60,7 +29,7 @@ it('boolVariation() calls base client with bound context', async () => {
6029
});
6130

6231
it('numberVariation() calls base client with bound context', async () => {
63-
const client = makeMockBaseClient();
32+
const client = makeMockServerClient();
6433
client.numberVariation.mockResolvedValue(42);
6534
const session = createLDServerSession(client, context);
6635
const result = await session.numberVariation('my-flag', 0);
@@ -69,7 +38,7 @@ it('numberVariation() calls base client with bound context', async () => {
6938
});
7039

7140
it('stringVariation() calls base client with bound context', async () => {
72-
const client = makeMockBaseClient();
41+
const client = makeMockServerClient();
7342
client.stringVariation.mockResolvedValue('hello');
7443
const session = createLDServerSession(client, context);
7544
const result = await session.stringVariation('my-flag', 'default');
@@ -78,7 +47,7 @@ it('stringVariation() calls base client with bound context', async () => {
7847
});
7948

8049
it('jsonVariation() calls base client with bound context', async () => {
81-
const client = makeMockBaseClient();
50+
const client = makeMockServerClient();
8251
const json = { key: 'value' };
8352
client.jsonVariation.mockResolvedValue(json);
8453
const session = createLDServerSession(client, context);
@@ -88,7 +57,7 @@ it('jsonVariation() calls base client with bound context', async () => {
8857
});
8958

9059
it('boolVariationDetail() calls base client with bound context', async () => {
91-
const client = makeMockBaseClient();
60+
const client = makeMockServerClient();
9261
const detail = { value: true, variationIndex: 1, reason: { kind: 'RULE_MATCH' as const } };
9362
// @ts-ignore — valid LDEvaluationDetailTyped<boolean> shape; mock type is too narrow
9463
client.boolVariationDetail.mockResolvedValue(detail);
@@ -99,7 +68,7 @@ it('boolVariationDetail() calls base client with bound context', async () => {
9968
});
10069

10170
it('numberVariationDetail() calls base client with bound context', async () => {
102-
const client = makeMockBaseClient();
71+
const client = makeMockServerClient();
10372
const detail = { value: 42, variationIndex: 1, reason: { kind: 'RULE_MATCH' as const } };
10473
// @ts-ignore — valid LDEvaluationDetailTyped<number> shape; mock type is too narrow
10574
client.numberVariationDetail.mockResolvedValue(detail);
@@ -110,7 +79,7 @@ it('numberVariationDetail() calls base client with bound context', async () => {
11079
});
11180

11281
it('stringVariationDetail() calls base client with bound context', async () => {
113-
const client = makeMockBaseClient();
82+
const client = makeMockServerClient();
11483
const detail = { value: 'hello', variationIndex: 1, reason: { kind: 'RULE_MATCH' as const } };
11584
// @ts-ignore — valid LDEvaluationDetailTyped<string> shape; mock type is too narrow
11685
client.stringVariationDetail.mockResolvedValue(detail);
@@ -121,7 +90,7 @@ it('stringVariationDetail() calls base client with bound context', async () => {
12190
});
12291

12392
it('jsonVariationDetail() calls base client with bound context', async () => {
124-
const client = makeMockBaseClient();
93+
const client = makeMockServerClient();
12594
const detail = {
12695
value: { key: 'value' },
12796
variationIndex: 1,
@@ -136,14 +105,14 @@ it('jsonVariationDetail() calls base client with bound context', async () => {
136105
});
137106

138107
it('allFlagsState() calls base client with bound context', async () => {
139-
const client = makeMockBaseClient();
108+
const client = makeMockServerClient();
140109
const session = createLDServerSession(client, context);
141110
await session.allFlagsState();
142111
expect(client.allFlagsState).toHaveBeenCalledWith(context, undefined);
143112
});
144113

145114
it('allFlagsState() forwards options to base client', async () => {
146-
const client = makeMockBaseClient();
115+
const client = makeMockServerClient();
147116
const session = createLDServerSession(client, context);
148117
const options = { clientSideOnly: true };
149118
await session.allFlagsState(options);
@@ -165,7 +134,7 @@ describe('given a browser environment (window defined)', () => {
165134
});
166135

167136
it('throws an error instead of returning a no-op session', () => {
168-
const client = makeMockBaseClient();
137+
const client = makeMockServerClient();
169138
expect(() => createLDServerSession(client, context)).toThrow(
170139
'createLDServerWrapper must only be called on the server.',
171140
);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { LDContext, LDFlagsStateOptions } from '@launchdarkly/js-server-sdk-common';
2+
3+
export function makeMockServerClient() {
4+
return {
5+
initialized: jest.fn(() => true),
6+
boolVariation: jest.fn((_key: string, _ctx: LDContext, def: boolean) => Promise.resolve(def)),
7+
numberVariation: jest.fn((_key: string, _ctx: LDContext, def: number) => Promise.resolve(def)),
8+
stringVariation: jest.fn((_key: string, _ctx: LDContext, def: string) => Promise.resolve(def)),
9+
jsonVariation: jest.fn((_key: string, _ctx: LDContext, def: unknown) => Promise.resolve(def)),
10+
boolVariationDetail: jest.fn((_key: string, _ctx: LDContext, def: boolean) =>
11+
Promise.resolve({ value: def, variationIndex: null, reason: { kind: 'OFF' as const } }),
12+
),
13+
numberVariationDetail: jest.fn((_key: string, _ctx: LDContext, def: number) =>
14+
Promise.resolve({ value: def, variationIndex: null, reason: { kind: 'OFF' as const } }),
15+
),
16+
stringVariationDetail: jest.fn((_key: string, _ctx: LDContext, def: string) =>
17+
Promise.resolve({ value: def, variationIndex: null, reason: { kind: 'OFF' as const } }),
18+
),
19+
jsonVariationDetail: jest.fn((_key: string, _ctx: LDContext, def: unknown) =>
20+
Promise.resolve({ value: def, variationIndex: null, reason: { kind: 'OFF' as const } }),
21+
),
22+
// @ts-ignore — mock return shape matches LDFlagsState structurally
23+
allFlagsState: jest.fn((_context: LDContext, _options?: LDFlagsStateOptions) =>
24+
Promise.resolve({
25+
valid: true,
26+
getFlagValue: jest.fn(),
27+
getFlagReason: jest.fn(),
28+
allValues: jest.fn(() => ({})),
29+
toJSON: jest.fn(() => ({ $flagsState: {}, $valid: true })),
30+
}),
31+
),
32+
track: jest.fn(() => Promise.resolve()),
33+
};
34+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { LDContext } from '@launchdarkly/js-server-sdk-common';
2+
3+
import { createLDServerWrapper } from '../../src/server/LDServerSession';
4+
import { makeMockServerClient } from './mockServerClient';
5+
6+
const context: LDContext = { kind: 'user', key: 'test-user' };
7+
8+
it('calls track once after the first variation call', async () => {
9+
const client = makeMockServerClient();
10+
const session = createLDServerWrapper(client, context);
11+
12+
await session.boolVariation('flag-1', false);
13+
14+
expect(client.track).toHaveBeenCalledTimes(1);
15+
expect(client.track).toHaveBeenCalledWith('$ld:react-sdk:rsc-evaluation', context);
16+
});
17+
18+
it('does not call track again on subsequent variation calls', async () => {
19+
const client = makeMockServerClient();
20+
const session = createLDServerWrapper(client, context);
21+
22+
await session.boolVariation('flag-1', false);
23+
await session.stringVariation('flag-2', 'default');
24+
await session.numberVariation('flag-3', 0);
25+
await session.jsonVariation('flag-4', {});
26+
await session.boolVariationDetail('flag-5', false);
27+
await session.numberVariationDetail('flag-6', 0);
28+
await session.stringVariationDetail('flag-7', 'default');
29+
await session.jsonVariationDetail('flag-8', {});
30+
31+
expect(client.track).toHaveBeenCalledTimes(1);
32+
});
33+
34+
it('does not call track if no variation calls are made', () => {
35+
const client = makeMockServerClient();
36+
createLDServerWrapper(client, context);
37+
38+
expect(client.track).not.toHaveBeenCalled();
39+
});
40+
41+
it('does not call track for allFlagsState', async () => {
42+
const client = makeMockServerClient();
43+
const session = createLDServerWrapper(client, context);
44+
45+
await session.allFlagsState();
46+
47+
expect(client.track).not.toHaveBeenCalled();
48+
});
49+
50+
it('does not call track for initialized', () => {
51+
const client = makeMockServerClient();
52+
const session = createLDServerWrapper(client, context);
53+
54+
session.initialized();
55+
56+
expect(client.track).not.toHaveBeenCalled();
57+
});

packages/sdk/react/examples/server-only/app/styles.css

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,20 @@
3030
flex-direction: column;
3131
align-items: center;
3232
justify-content: center;
33-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
33+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
3434
font-size: calc(10px + 2vmin);
3535
padding: 2rem;
3636
gap: 1rem;
3737
}
3838

39-
.app--on { background-color: #00844B; }
40-
.app--off { background-color: #373841; }
39+
.app--on {
40+
background-color: #00844b;
41+
}
42+
.app--off {
43+
background-color: #373841;
44+
}
4145

4246
.context {
4347
font-size: 0.7em;
4448
opacity: 0.75;
4549
}
46-
Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
{
22
"compilerOptions": {
33
"target": "ES2017",
4-
"lib": [
5-
"dom",
6-
"dom.iterable",
7-
"esnext"
8-
],
4+
"lib": ["dom", "dom.iterable", "esnext"],
95
"allowJs": true,
106
"skipLibCheck": true,
117
"strict": true,
@@ -23,9 +19,7 @@
2319
}
2420
],
2521
"paths": {
26-
"@/*": [
27-
"./*"
28-
]
22+
"@/*": ["./*"]
2923
}
3024
},
3125
"include": [
@@ -35,9 +29,5 @@
3529
".next/types/**/*.ts",
3630
".next/dev/types/**/*.ts"
3731
],
38-
"exclude": [
39-
"node_modules",
40-
"e2e",
41-
"playwright.config.ts"
42-
]
32+
"exclude": ["node_modules", "e2e", "playwright.config.ts"]
4333
}

packages/sdk/react/src/server/LDServerBaseClient.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,9 @@ export interface LDServerBaseClient {
8888
* Builds an object encapsulating the state of all feature flags for a given context.
8989
*/
9090
allFlagsState(context: LDContext, options?: LDFlagsStateOptions): Promise<LDFlagsState>;
91+
92+
/**
93+
* Tracks that a context performed an event.
94+
*/
95+
track(key: string, context: LDContext, data?: any, metricValue?: number): void;
9196
}

packages/sdk/react/src/server/LDServerSession.ts

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,21 +57,53 @@ export function createLDServerWrapper(
5757
);
5858
}
5959

60+
// Batch in a single event so we can track the usage of this SDK for React Server Components.
61+
let tracked = false;
62+
63+
function trackRscUsage() {
64+
if (tracked) {
65+
return;
66+
}
67+
tracked = true;
68+
// TODO: placeholder event name for now until we are sure this is a good idea.
69+
client.track('$ld:react-sdk:rsc-evaluation', context);
70+
}
71+
6072
return {
6173
initialized: () => client.initialized(),
6274
getContext: () => context,
63-
boolVariation: (key, defaultValue) => client.boolVariation(key, context, defaultValue),
64-
numberVariation: (key, defaultValue) => client.numberVariation(key, context, defaultValue),
65-
stringVariation: (key, defaultValue) => client.stringVariation(key, context, defaultValue),
66-
jsonVariation: (key, defaultValue) => client.jsonVariation(key, context, defaultValue),
67-
boolVariationDetail: (key, defaultValue) =>
68-
client.boolVariationDetail(key, context, defaultValue),
69-
numberVariationDetail: (key, defaultValue) =>
70-
client.numberVariationDetail(key, context, defaultValue),
71-
stringVariationDetail: (key, defaultValue) =>
72-
client.stringVariationDetail(key, context, defaultValue),
73-
jsonVariationDetail: (key, defaultValue) =>
74-
client.jsonVariationDetail(key, context, defaultValue),
75+
boolVariation: (key, defaultValue) => {
76+
trackRscUsage();
77+
return client.boolVariation(key, context, defaultValue);
78+
},
79+
numberVariation: (key, defaultValue) => {
80+
trackRscUsage();
81+
return client.numberVariation(key, context, defaultValue);
82+
},
83+
stringVariation: (key, defaultValue) => {
84+
trackRscUsage();
85+
return client.stringVariation(key, context, defaultValue);
86+
},
87+
jsonVariation: (key, defaultValue) => {
88+
trackRscUsage();
89+
return client.jsonVariation(key, context, defaultValue);
90+
},
91+
boolVariationDetail: (key, defaultValue) => {
92+
trackRscUsage();
93+
return client.boolVariationDetail(key, context, defaultValue);
94+
},
95+
numberVariationDetail: (key, defaultValue) => {
96+
trackRscUsage();
97+
return client.numberVariationDetail(key, context, defaultValue);
98+
},
99+
stringVariationDetail: (key, defaultValue) => {
100+
trackRscUsage();
101+
return client.stringVariationDetail(key, context, defaultValue);
102+
},
103+
jsonVariationDetail: (key, defaultValue) => {
104+
trackRscUsage();
105+
return client.jsonVariationDetail(key, context, defaultValue);
106+
},
75107
allFlagsState: (options?: LDFlagsStateOptions) => client.allFlagsState(context, options),
76108
};
77109
}

0 commit comments

Comments
 (0)