Skip to content

Commit b5b11a1

Browse files
committed
fix: only emit resolve helper log once per flag+context
1 parent 3c764d9 commit b5b11a1

File tree

4 files changed

+77
-9
lines changed

4 files changed

+77
-9
lines changed

api/sdk.api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export interface CacheOptions {
1313
//
1414
// @internal (undocumented)
1515
entries?: AsyncIterable<CacheEntry>;
16+
// @internal
17+
loggedFlags?: Set<string>;
1618
scope?: CacheScope;
1719
}
1820

packages/sdk/src/Confidence.test.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,28 @@ describe('Confidence', () => {
2121
variant: 'mockVariant',
2222
};
2323

24+
const loggerSpy = {
25+
infoLogs: [] as string[],
26+
info: (input: string) => {
27+
loggerSpy.infoLogs.push(input);
28+
},
29+
};
30+
2431
beforeEach(() => {
32+
loggerSpy.infoLogs = [];
2533
confidence = new Confidence({
2634
clientSecret: 'secret',
2735
timeout: 10,
2836
environment: 'client',
29-
logger: {},
37+
logger: loggerSpy,
3038
eventSenderEngine: eventSenderEngineMock,
3139
flagResolverClient: flagResolverClientMock,
3240
cacheProvider: () => {
3341
throw new Error('Not implemented');
3442
},
43+
cache: {
44+
loggedFlags: new Set(),
45+
},
3546
});
3647
flagResolverClientMock.resolve.mockImplementation((context, _flags) => {
3748
const flagResolution = new Promise<FlagResolution>(resolve => {
@@ -377,5 +388,32 @@ describe('Confidence', () => {
377388
variant: 'mockVariant',
378389
});
379390
});
391+
392+
it('should log the flag resolve hint once per and context and flag', async () => {
393+
const ctx = { targeting_key: 'default', pantsOn: true, pantsColor: 'blue' };
394+
const c = confidence.withContext(ctx);
395+
await c.evaluateFlag('flag1', 'default');
396+
c.getFlag('flag1', 'default');
397+
398+
expect(loggerSpy.infoLogs.length).toEqual(1);
399+
expect(loggerSpy.infoLogs[0]).toEqual(
400+
"See resolves for 'flag1' in Confidence: https://app.confidence.spotify.com/flags/resolver-test?client-key=secret&flag=flags/flag1&context=%7B%22targeting_key%22%3A%22default%22%2C%22pantsOn%22%3Atrue%2C%22pantsColor%22%3A%22blue%22%7D",
401+
);
402+
c.getFlag('flag1', 'default');
403+
expect(loggerSpy.infoLogs.length).toEqual(1);
404+
405+
await c.evaluateFlag('flag2', 'default');
406+
expect(loggerSpy.infoLogs.length).toEqual(2);
407+
expect(loggerSpy.infoLogs[1]).toEqual(
408+
"See resolves for 'flag2' in Confidence: https://app.confidence.spotify.com/flags/resolver-test?client-key=secret&flag=flags/flag2&context=%7B%22targeting_key%22%3A%22default%22%2C%22pantsOn%22%3Atrue%2C%22pantsColor%22%3A%22blue%22%7D",
409+
);
410+
const c2 = c.withContext({ pantsOn: false });
411+
await c2.evaluateFlag('flag2', 'default');
412+
c2.getFlag('flag2', 'default');
413+
expect(loggerSpy.infoLogs.length).toEqual(3);
414+
expect(loggerSpy.infoLogs[2]).toEqual(
415+
"See resolves for 'flag2' in Confidence: https://app.confidence.spotify.com/flags/resolver-test?client-key=secret&flag=flags/flag2&context=%7B%22targeting_key%22%3A%22default%22%2C%22pantsColor%22%3A%22blue%22%2C%22pantsOn%22%3Afalse%7D",
416+
);
417+
});
380418
});
381419
});

packages/sdk/src/Confidence.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -329,11 +329,15 @@ export class Confidence implements EventSender, Trackable, FlagResolver {
329329
}
330330

331331
private showLoggerLink(flag: string, context: Context) {
332-
this.config.logger.info?.(
333-
`See resolves for '${flag}' in Confidence: https://app.confidence.spotify.com/flags/resolver-test?client-key=${
334-
this.config.clientSecret
335-
}&flag=flags/${flag}&context=${encodeURIComponent(JSON.stringify(context))}`,
336-
);
332+
const logKey = `${flag}:${Value.serialize(context)}`;
333+
if (!this.config.cache?.loggedFlags?.has(logKey)) {
334+
this.config.cache?.loggedFlags?.add(logKey);
335+
this.config.logger.info?.(
336+
`See resolves for '${flag}' in Confidence: https://app.confidence.spotify.com/flags/resolver-test?client-key=${
337+
this.config.clientSecret
338+
}&flag=flags/${flag}&context=${encodeURIComponent(JSON.stringify(context))}`,
339+
);
340+
}
337341
}
338342

339343
toOptions(): ConfidenceOptions {
@@ -371,7 +375,9 @@ export class Confidence implements EventSender, Trackable, FlagResolver {
371375
disableTelemetry = false,
372376
applyDebounce = 10,
373377
waitUntil,
374-
cache = {},
378+
cache = {
379+
loggedFlags: new Set(),
380+
},
375381
} = options;
376382
if (environment !== 'client' && environment !== 'backend') {
377383
throw new Error(`Invalid environment: ${environment}. Must be 'client' or 'backend'.`);

packages/sdk/src/flag-cache.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,26 @@ export interface CacheOptions {
3636
scope?: CacheScope;
3737
/** @internal */
3838
entries?: AsyncIterable<CacheEntry>;
39+
/**
40+
* Flags that have been logged using the showLoggerLink method.
41+
* @internal
42+
*/
43+
loggedFlags?: Set<string>;
3944
}
45+
46+
type FlagCacheOptions = {
47+
loggedFlags?: Set<string>;
48+
};
4049
export class FlagCache extends AbstractCache<Context, ResolveFlagsResponse, Uint8Array> {
50+
/**
51+
* Flags that have been logged using the showLoggerLink method.
52+
* @internal
53+
*/
54+
private readonly logs: Set<string>;
55+
constructor(options: FlagCacheOptions) {
56+
super();
57+
this.logs = options.loggedFlags ?? new Set();
58+
}
4159
protected serialize(value: ResolveFlagsResponse): Uint8Array {
4260
return ResolveFlagsResponse.encode(value).finish();
4361
}
@@ -62,6 +80,7 @@ export class FlagCache extends AbstractCache<Context, ResolveFlagsResponse, Uint
6280
toOptions(): CacheOptions {
6381
return {
6482
entries: this,
83+
loggedFlags: this.logs,
6584
};
6685
}
6786
}
@@ -85,8 +104,11 @@ export namespace FlagCache {
85104
return typeof window === 'undefined' ? noScope : singletonScope;
86105
}
87106

88-
export function provider(clientKey: string, { scope = defaultScope(), entries }: CacheOptions): CacheProvider {
89-
const provider = scope(() => new FlagCache());
107+
export function provider(
108+
clientKey: string,
109+
{ scope = defaultScope(), entries, loggedFlags }: CacheOptions,
110+
): CacheProvider {
111+
const provider = scope(() => new FlagCache({ loggedFlags }));
90112
if (entries) {
91113
provider(clientKey).load(entries);
92114
}

0 commit comments

Comments
 (0)