diff --git a/src/lib/features/playground/clean-context.test.ts b/src/lib/features/playground/clean-context.test.ts index bf32cacf98..74f672739f 100644 --- a/src/lib/features/playground/clean-context.test.ts +++ b/src/lib/features/playground/clean-context.test.ts @@ -1,15 +1,15 @@ import { cleanContext } from './clean-context'; -test('strips invalid context properties from the context', async () => { - const invalidJsonTypes = { - object: {}, - array: [], - true: true, - false: false, - number: 123, - null: null, - }; +const invalidJsonTypes = { + object: {}, + array: [], + true: true, + false: false, + number: 123, + null: null, +}; +test('strips invalid context properties from the context', async () => { const validValues = { appName: 'test', }; @@ -19,7 +19,7 @@ test('strips invalid context properties from the context', async () => { ...validValues, }; - const cleanedContext = cleanContext(inputContext); + const { context: cleanedContext } = cleanContext(inputContext); expect(cleanedContext).toStrictEqual(validValues); }); @@ -29,7 +29,25 @@ test("doesn't add non-existing properties", async () => { appName: 'test', }; - const output = cleanContext(input); + const { context: output } = cleanContext(input); expect(output).toStrictEqual(input); }); + +test('it returns the names of all the properties it removed', async () => { + const { removedProperties } = cleanContext({ + appName: 'test', + ...invalidJsonTypes, + }); + + const invalidProperties = Object.keys(invalidJsonTypes); + + // verify that the two lists contain all the same elements + expect(removedProperties).toEqual( + expect.arrayContaining(invalidProperties), + ); + + expect(invalidProperties).toEqual( + expect.arrayContaining(removedProperties), + ); +}); diff --git a/src/lib/features/playground/clean-context.ts b/src/lib/features/playground/clean-context.ts index 238c0b0960..e9055fd470 100644 --- a/src/lib/features/playground/clean-context.ts +++ b/src/lib/features/playground/clean-context.ts @@ -1,16 +1,26 @@ import type { SdkContextSchema } from '../../openapi'; -export const cleanContext = (context: SdkContextSchema): SdkContextSchema => { +export const cleanContext = ( + context: SdkContextSchema, +): { context: SdkContextSchema; removedProperties: string[] } => { const { appName, ...otherContextFields } = context; + const removedProperties: string[] = []; const cleanedContextFields = Object.fromEntries( - Object.entries(otherContextFields).filter( - ([key, value]) => key === 'properties' || typeof value === 'string', - ), + Object.entries(otherContextFields).filter(([key, value]) => { + if (key === 'properties' || typeof value === 'string') { + return true; + } + removedProperties.push(key); + return false; + }), ); return { - ...cleanedContextFields, - appName, + context: { + ...cleanedContextFields, + appName, + }, + removedProperties, }; }; diff --git a/src/lib/features/playground/playground-api.e2e.test.ts b/src/lib/features/playground/playground-api.e2e.test.ts index fd2d747cec..de13b37267 100644 --- a/src/lib/features/playground/playground-api.e2e.test.ts +++ b/src/lib/features/playground/playground-api.e2e.test.ts @@ -76,3 +76,30 @@ test('returns the input context exactly as it came in, even if invalid values ha expect(body.input.context).toMatchObject(inputContext); }); + +test('adds all removed top-level context properties to the list of warnings', async () => { + const invalidData = { + invalid1: {}, + invalid2: {}, + }; + + const inputContext = { + ...invalidData, + appName: 'test', + }; + + const { body } = await app.request + .post('/api/admin/playground/advanced') + .send({ + context: inputContext, + environments: ['production'], + projects: '*', + }) + .expect(200); + + const warned = body.warnings.invalidContextProperties; + const invalidKeys = Object.keys(invalidData); + + expect(warned).toEqual(expect.arrayContaining(invalidKeys)); + expect(invalidKeys).toEqual(expect.arrayContaining(warned)); +}); diff --git a/src/lib/features/playground/playground-service.ts b/src/lib/features/playground/playground-service.ts index a04b05828a..dd3bc2b17f 100644 --- a/src/lib/features/playground/playground-service.ts +++ b/src/lib/features/playground/playground-service.ts @@ -103,7 +103,10 @@ export class PlaygroundService { context: SdkContextSchema, limit: number, userId: number, - ): Promise { + ): Promise<{ + result: AdvancedPlaygroundFeatureEvaluationResult[]; + invalidContextProperties: string[]; + }> { const segments = await this.segmentReadModel.getActive(); let filteredProjects: typeof projects = projects; @@ -126,7 +129,9 @@ export class PlaygroundService { ), ); - const contexts = generateObjectCombinations(cleanContext(context)); + const { context: cleanedContext, removedProperties } = + cleanContext(context); + const contexts = generateObjectCombinations(cleanedContext); validateQueryComplexity( environments.length, @@ -151,7 +156,7 @@ export class PlaygroundService { ); const items = results.flat(); const itemsByName = groupBy(items, (item) => item.name); - return Object.values(itemsByName).map((entries) => { + const result = Object.values(itemsByName).map((entries) => { const groupedEnvironments = groupBy( entries, (entry) => entry.environment, @@ -162,6 +167,11 @@ export class PlaygroundService { environments: groupedEnvironments, }; }); + + return { + result, + invalidContextProperties: removedProperties, + }; } private async evaluate({ diff --git a/src/lib/features/playground/playground-view-model.ts b/src/lib/features/playground/playground-view-model.ts index a7cdcce7e9..2b0e7b5f91 100644 --- a/src/lib/features/playground/playground-view-model.ts +++ b/src/lib/features/playground/playground-view-model.ts @@ -40,6 +40,7 @@ const addStrategyEditLink = ( export const advancedPlaygroundViewModel = ( input: AdvancedPlaygroundRequestSchema, playgroundResult: AdvancedPlaygroundFeatureEvaluationResult[], + invalidContextProperties?: string[], ): AdvancedPlaygroundResponseSchema => { const features = playgroundResult.map(({ environments, ...rest }) => { const transformedEnvironments = Object.entries(environments).map( @@ -79,6 +80,10 @@ export const advancedPlaygroundViewModel = ( }; }); + if (invalidContextProperties?.length) { + return { features, input, warnings: { invalidContextProperties } }; + } + return { features, input }; }; diff --git a/src/lib/features/playground/playground.ts b/src/lib/features/playground/playground.ts index 41f5da2cd5..178929808a 100644 --- a/src/lib/features/playground/playground.ts +++ b/src/lib/features/playground/playground.ts @@ -125,16 +125,21 @@ export default class PlaygroundController extends Controller { ? Number.parseInt(payload?.value) : 15000; - const result = await this.playgroundService.evaluateAdvancedQuery( - req.body.projects || '*', - req.body.environments, - req.body.context, - limit, - extractUserIdFromUser(user), - ); + const { result, invalidContextProperties } = + await this.playgroundService.evaluateAdvancedQuery( + req.body.projects || '*', + req.body.environments, + req.body.context, + limit, + extractUserIdFromUser(user), + ); const response: AdvancedPlaygroundResponseSchema = - advancedPlaygroundViewModel(req.body, result); + advancedPlaygroundViewModel( + req.body, + result, + invalidContextProperties, + ); res.json(response); } diff --git a/src/lib/openapi/spec/advanced-playground-response-schema.ts b/src/lib/openapi/spec/advanced-playground-response-schema.ts index 9854a4c6af..b27bdf7b00 100644 --- a/src/lib/openapi/spec/advanced-playground-response-schema.ts +++ b/src/lib/openapi/spec/advanced-playground-response-schema.ts @@ -30,6 +30,20 @@ export const advancedPlaygroundResponseSchema = { $ref: advancedPlaygroundFeatureSchema.$id, }, }, + warnings: { + type: 'object', + description: 'Warnings that occurred during evaluation.', + properties: { + invalidContextProperties: { + type: 'array', + description: + 'A list of top-level context properties that were provided as input that are not valid due to being the wrong type.', + items: { + type: 'string', + }, + }, + }, + }, }, components: { schemas: {