Skip to content
Open
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
2 changes: 2 additions & 0 deletions api/sdk.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export interface CacheOptions {
//
// @internal (undocumented)
entries?: AsyncIterable<CacheEntry>;
// @internal
loggedFlags?: Set<string>;
scope?: CacheScope;
}

Expand Down
40 changes: 39 additions & 1 deletion packages/sdk/src/Confidence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,28 @@ describe('Confidence', () => {
variant: 'mockVariant',
};

const loggerSpy = {
infoLogs: [] as string[],
info: (input: string) => {
loggerSpy.infoLogs.push(input);
},
};

beforeEach(() => {
loggerSpy.infoLogs = [];
confidence = new Confidence({
clientSecret: 'secret',
timeout: 10,
environment: 'client',
logger: {},
logger: loggerSpy,
eventSenderEngine: eventSenderEngineMock,
flagResolverClient: flagResolverClientMock,
cacheProvider: () => {
throw new Error('Not implemented');
},
cache: {
loggedFlags: new Set(),
},
});
flagResolverClientMock.resolve.mockImplementation((context, _flags) => {
const flagResolution = new Promise<FlagResolution>(resolve => {
Expand Down Expand Up @@ -377,5 +388,32 @@ describe('Confidence', () => {
variant: 'mockVariant',
});
});

it('should log the flag resolve hint once per and context and flag', async () => {
const ctx = { targeting_key: 'default', pantsOn: true, pantsColor: 'blue' };
const c = confidence.withContext(ctx);
await c.evaluateFlag('flag1', 'default');
c.getFlag('flag1', 'default');

expect(loggerSpy.infoLogs.length).toEqual(1);
expect(loggerSpy.infoLogs[0]).toEqual(
"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",
);
c.getFlag('flag1', 'default');
expect(loggerSpy.infoLogs.length).toEqual(1);

await c.evaluateFlag('flag2', 'default');
expect(loggerSpy.infoLogs.length).toEqual(2);
expect(loggerSpy.infoLogs[1]).toEqual(
"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",
);
const c2 = c.withContext({ pantsOn: false });
await c2.evaluateFlag('flag2', 'default');
c2.getFlag('flag2', 'default');
expect(loggerSpy.infoLogs.length).toEqual(3);
expect(loggerSpy.infoLogs[2]).toEqual(
"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",
);
});
});
});
18 changes: 12 additions & 6 deletions packages/sdk/src/Confidence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,11 +329,15 @@ export class Confidence implements EventSender, Trackable, FlagResolver {
}

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

toOptions(): ConfidenceOptions {
Expand Down Expand Up @@ -371,7 +375,9 @@ export class Confidence implements EventSender, Trackable, FlagResolver {
disableTelemetry = false,
applyDebounce = 10,
waitUntil,
cache = {},
cache = {
loggedFlags: new Set(),
},
} = options;
if (environment !== 'client' && environment !== 'backend') {
throw new Error(`Invalid environment: ${environment}. Must be 'client' or 'backend'.`);
Expand Down
26 changes: 24 additions & 2 deletions packages/sdk/src/flag-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,26 @@ export interface CacheOptions {
scope?: CacheScope;
/** @internal */
entries?: AsyncIterable<CacheEntry>;
/**
* Flags that have been logged using the showLoggerLink method.
* @internal
*/
loggedFlags?: Set<string>;
}

type FlagCacheOptions = {
loggedFlags?: Set<string>;
};
export class FlagCache extends AbstractCache<Context, ResolveFlagsResponse, Uint8Array> {
/**
* Flags that have been logged using the showLoggerLink method.
* @internal
*/
private readonly logs: Set<string>;
constructor(options: FlagCacheOptions) {
super();
this.logs = options.loggedFlags ?? new Set();
}
protected serialize(value: ResolveFlagsResponse): Uint8Array {
return ResolveFlagsResponse.encode(value).finish();
}
Expand All @@ -62,6 +80,7 @@ export class FlagCache extends AbstractCache<Context, ResolveFlagsResponse, Uint
toOptions(): CacheOptions {
return {
entries: this,
loggedFlags: this.logs,
};
}
}
Expand All @@ -85,8 +104,11 @@ export namespace FlagCache {
return typeof window === 'undefined' ? noScope : singletonScope;
}

export function provider(clientKey: string, { scope = defaultScope(), entries }: CacheOptions): CacheProvider {
const provider = scope(() => new FlagCache());
export function provider(
clientKey: string,
{ scope = defaultScope(), entries, loggedFlags }: CacheOptions,
): CacheProvider {
const provider = scope(() => new FlagCache({ loggedFlags }));
if (entries) {
provider(clientKey).load(entries);
}
Expand Down