import { TargetAccessScope, ProjectType, ProjectAccessScope, OrganizationAccessScope } from '@app/gql/graphql'; import formatISO from 'date-fns/formatISO'; import subHours from 'date-fns/subHours'; import { createOrganization, createProject, createTarget, createToken, publishSchema, checkSchema, setTargetValidation, updateTargetValidationSettings, readOperationsStats, waitFor, } from '../../../testkit/flow'; import { authenticate } from '../../../testkit/auth'; import { collect, CollectedOperation } from '../../../testkit/usage'; import { clickHouseQuery } from '../../../testkit/clickhouse'; // eslint-disable-next-line hive/enforce-deps-in-dev, import/no-extraneous-dependencies import { normalizeOperation } from '@graphql-hive/core'; // eslint-disable-next-line import/no-extraneous-dependencies import { parse, print } from 'graphql'; function ensureNumber(value: number | string): number { if (typeof value === 'number') { return value; } return parseFloat(value); } const FF_CLICKHOUSE_V2_TABLES = process.env.FF_CLICKHOUSE_V2_TABLES === '1'; if (FF_CLICKHOUSE_V2_TABLES) { console.log('Using FF_CLICKHOUSE_V2_TABLES'); } function sendBatch(amount: number, operation: CollectedOperation, token: string) { return collect({ operations: new Array(amount).fill(operation), token, }); } test('collect operation', async () => { const { access_token: owner_access_token } = await authenticate('main'); const orgResult = await createOrganization( { name: 'foo', }, owner_access_token ); const org = orgResult.body.data!.createOrganization.ok!.createdOrganizationPayload.organization; const projectResult = await createProject( { organization: org.cleanId, type: ProjectType.Single, name: 'foo', }, owner_access_token ); const project = projectResult.body.data!.createProject.ok!.createdProject; const target = projectResult.body.data!.createProject.ok!.createdTargets[0]; const settingsTokenResult = await createToken( { name: 'test-settings', organization: org.cleanId, project: project.cleanId, target: target.cleanId, organizationScopes: [OrganizationAccessScope.Read], projectScopes: [ProjectAccessScope.Read], targetScopes: [TargetAccessScope.Read, TargetAccessScope.Settings], }, owner_access_token ); const tokenResult = await createToken( { name: 'test', organization: org.cleanId, project: project.cleanId, target: target.cleanId, organizationScopes: [OrganizationAccessScope.Read], projectScopes: [ProjectAccessScope.Read], targetScopes: [TargetAccessScope.Read, TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], }, owner_access_token ); expect(settingsTokenResult.body.errors).not.toBeDefined(); expect(tokenResult.body.errors).not.toBeDefined(); const token = tokenResult.body.data!.createToken.ok!.secret; const tokenForSettings = settingsTokenResult.body.data!.createToken.ok!.secret; const schemaPublishResult = await publishSchema( { author: 'Kamil', commit: 'abc123', sdl: `type Query { ping: String me: String }`, }, token ); expect(schemaPublishResult.body.errors).not.toBeDefined(); expect((schemaPublishResult.body.data!.schemaPublish as any).valid).toEqual(true); const targetValidationResult = await setTargetValidation( { enabled: true, organization: org.cleanId, project: project.cleanId, target: target.cleanId, }, { token: tokenForSettings, } ); expect(targetValidationResult.body.errors).not.toBeDefined(); expect(targetValidationResult.body.data!.setTargetValidation.enabled).toEqual(true); expect(targetValidationResult.body.data!.setTargetValidation.percentage).toEqual(0); expect(targetValidationResult.body.data!.setTargetValidation.period).toEqual(30); // should not be breaking because the field is unused const unusedCheckResult = await checkSchema( { sdl: `type Query { me: String }`, }, token ); expect(unusedCheckResult.body.errors).not.toBeDefined(); expect(unusedCheckResult.body.data!.schemaCheck.__typename).toEqual('SchemaCheckSuccess'); const collectResult = await collect({ operations: [ { operation: 'query ping { ping }', operationName: 'ping', fields: ['Query', 'Query.ping'], execution: { ok: true, duration: 200000000, errorsTotal: 0, }, }, ], token, }); expect(collectResult.status).toEqual(200); await waitFor(5_000); // should be breaking because the field is used now const usedCheckResult = await checkSchema( { sdl: `type Query { me: String }`, }, token ); if (usedCheckResult.body.data!.schemaCheck.__typename !== 'SchemaCheckError') { throw new Error(`Expected SchemaCheckError, got ${usedCheckResult.body.data!.schemaCheck.__typename}`); } expect(usedCheckResult.body.data!.schemaCheck.valid).toEqual(false); const from = formatISO(subHours(Date.now(), 6)); const to = formatISO(Date.now()); const operationStatsResult = await readOperationsStats( { organization: org.cleanId, project: project.cleanId, target: target.cleanId, period: { from, to, }, }, token ); expect(operationStatsResult.body.errors).not.toBeDefined(); const operationsStats = operationStatsResult.body.data!.operationsStats; expect(operationsStats.operations.nodes).toHaveLength(1); const op = operationsStats.operations.nodes[0]; expect(op.count).toEqual(1); expect(op.document).toMatch('ping'); expect(op.operationHash).toBeDefined(); expect(op.duration.p75).toEqual(200); expect(op.duration.p90).toEqual(200); expect(op.duration.p95).toEqual(200); expect(op.duration.p99).toEqual(200); expect(op.kind).toEqual('query'); expect(op.name).toMatch('ping'); expect(op.percentage).toBeGreaterThan(99); }); test('normalize and collect operation without breaking its syntax', async () => { const { access_token: owner_access_token } = await authenticate('main'); const orgResult = await createOrganization( { name: 'foo', }, owner_access_token ); const org = orgResult.body.data!.createOrganization.ok!.createdOrganizationPayload.organization; const projectResult = await createProject( { organization: org.cleanId, type: ProjectType.Single, name: 'foo', }, owner_access_token ); const project = projectResult.body.data!.createProject.ok!.createdProject; const target = projectResult.body.data!.createProject.ok!.createdTargets[0]; const settingsTokenResult = await createToken( { name: 'test-settings', organization: org.cleanId, project: project.cleanId, target: target.cleanId, organizationScopes: [OrganizationAccessScope.Read], projectScopes: [ProjectAccessScope.Read], targetScopes: [TargetAccessScope.Read, TargetAccessScope.Settings], }, owner_access_token ); const tokenResult = await createToken( { name: 'test', organization: org.cleanId, project: project.cleanId, target: target.cleanId, organizationScopes: [OrganizationAccessScope.Read], projectScopes: [ProjectAccessScope.Read], targetScopes: [TargetAccessScope.Read, TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], }, owner_access_token ); expect(settingsTokenResult.body.errors).not.toBeDefined(); expect(tokenResult.body.errors).not.toBeDefined(); const token = tokenResult.body.data!.createToken.ok!.secret; const raw_document = ` query outfit { recommendations( input: { strategies: [{ name: "asd" }] articleId: "asd" customerId: "asd" phoenixEnabled: true sessionId: "asd" } ) { ... on RecommendationResponse { frequentlyBoughtTogether { recommendedProducts { id } strategyMessage } outfit { strategyMessage } outfit { recommendedProducts { articleId id imageUrl name productUrl rating tCode } strategyMessage } similar { recommendedProducts { articleId id imageUrl name productUrl rating tCode } strategyMessage } visualSearch { strategyMessage } } } } `; const normalized_document = normalizeOperation({ document: parse(raw_document), operationName: 'outfit', hideLiterals: true, removeAliases: true, }); const collectResult = await collect({ operations: [ { operation: normalizeOperation({ document: parse(raw_document), operationName: 'outfit', hideLiterals: true, removeAliases: true, }), operationName: 'outfit', fields: ['Query', 'Query.ping'], execution: { ok: true, duration: 200000000, errorsTotal: 0, }, }, ], token, }); expect(collectResult.status).toEqual(200); await waitFor(5_000); const from = formatISO(subHours(Date.now(), 6)); const to = formatISO(Date.now()); const operationStatsResult = await readOperationsStats( { organization: org.cleanId, project: project.cleanId, target: target.cleanId, period: { from, to, }, }, token ); expect(operationStatsResult.body.errors).not.toBeDefined(); const operationsStats = operationStatsResult.body.data!.operationsStats; expect(operationsStats.operations.nodes).toHaveLength(1); const op = operationsStats.operations.nodes[0]; expect(op.count).toEqual(1); expect(() => { parse(op.document); }).not.toThrow(); expect(print(parse(op.document))).toEqual(print(parse(normalized_document))); expect(op.operationHash).toBeDefined(); expect(op.duration.p75).toEqual(200); expect(op.duration.p90).toEqual(200); expect(op.duration.p95).toEqual(200); expect(op.duration.p99).toEqual(200); expect(op.kind).toEqual('query'); expect(op.name).toMatch('outfit'); expect(op.percentage).toBeGreaterThan(99); }); test('number of produced and collected operations should match (no errors)', async () => { const { access_token: owner_access_token } = await authenticate('main'); const orgResult = await createOrganization( { name: 'foo', }, owner_access_token ); const org = orgResult.body.data!.createOrganization.ok!.createdOrganizationPayload.organization; const projectResult = await createProject( { organization: org.cleanId, type: ProjectType.Single, name: 'foo', }, owner_access_token ); const project = projectResult.body.data!.createProject.ok!.createdProject; const target = projectResult.body.data!.createProject.ok!.createdTargets[0]; const tokenResult = await createToken( { name: 'test', organization: org.cleanId, project: project.cleanId, target: target.cleanId, organizationScopes: [OrganizationAccessScope.Read], projectScopes: [ProjectAccessScope.Read], targetScopes: [TargetAccessScope.Read, TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], }, owner_access_token ); expect(tokenResult.body.errors).not.toBeDefined(); const token = tokenResult.body.data!.createToken.ok!.secret; const batchSize = 1000; const totalAmount = 10_000; // eslint-disable-next-line @typescript-eslint/no-unused-vars for await (const _ of new Array(totalAmount / batchSize)) { await sendBatch( batchSize, { operation: 'query ping { ping }', operationName: 'ping', fields: ['Query', 'Query.ping'], execution: { ok: true, duration: 200000000, errorsTotal: 0, }, }, token ); } await waitFor(5_000); const from = formatISO(subHours(Date.now(), 6)); const to = formatISO(Date.now()); const operationStatsResult = await readOperationsStats( { organization: org.cleanId, project: project.cleanId, target: target.cleanId, period: { from, to, }, }, token ); expect(operationStatsResult.body.errors).not.toBeDefined(); const operationsStats = operationStatsResult.body.data!.operationsStats; // We sent a single operation (multiple times) expect(operationsStats.operations.nodes).toHaveLength(1); const op = operationsStats.operations.nodes[0]; expect(op.count).toEqual(totalAmount); expect(op.document).toMatch('ping'); expect(op.operationHash).toBeDefined(); expect(op.duration.p75).toEqual(200); expect(op.duration.p90).toEqual(200); expect(op.duration.p95).toEqual(200); expect(op.duration.p99).toEqual(200); expect(op.kind).toEqual('query'); expect(op.name).toMatch('ping'); expect(op.percentage).toBeGreaterThan(99); }); test('check usage from two selected targets', async () => { const { access_token: owner_access_token } = await authenticate('main'); const orgResult = await createOrganization( { name: 'foo', }, owner_access_token ); const org = orgResult.body.data!.createOrganization.ok!.createdOrganizationPayload.organization; const projectResult = await createProject( { organization: org.cleanId, type: ProjectType.Single, name: 'foo', }, owner_access_token ); const project = projectResult.body.data!.createProject.ok!.createdProject; const staging = projectResult.body.data!.createProject.ok!.createdTargets[0]; const productionTargetResult = await createTarget( { name: 'production', organization: org.cleanId, project: project.cleanId, }, owner_access_token ); const production = productionTargetResult.body.data!.createTarget.ok!.createdTarget; const stagingTokenResult = await createToken( { name: 'test', organization: org.cleanId, project: project.cleanId, target: staging.cleanId, organizationScopes: [OrganizationAccessScope.Read], projectScopes: [ProjectAccessScope.Read], targetScopes: [TargetAccessScope.Read, TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], }, owner_access_token ); const productionTokenResult = await createToken( { name: 'test', organization: org.cleanId, project: project.cleanId, target: production.cleanId, organizationScopes: [OrganizationAccessScope.Read], projectScopes: [ProjectAccessScope.Read], targetScopes: [TargetAccessScope.Read, TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], }, owner_access_token ); expect(stagingTokenResult.body.errors).not.toBeDefined(); expect(productionTokenResult.body.errors).not.toBeDefined(); const tokenForStaging = stagingTokenResult.body.data!.createToken.ok!.secret; const tokenForProduction = productionTokenResult.body.data!.createToken.ok!.secret; const schemaPublishResult = await publishSchema( { author: 'Kamil', commit: 'usage-check-2', sdl: `type Query { ping: String me: String }`, }, tokenForStaging ); expect(schemaPublishResult.body.errors).not.toBeDefined(); expect((schemaPublishResult.body.data!.schemaPublish as any).valid).toEqual(true); const targetValidationResult = await setTargetValidation( { enabled: true, organization: org.cleanId, project: project.cleanId, target: staging.cleanId, }, { authToken: owner_access_token, } ); expect(targetValidationResult.body.errors).not.toBeDefined(); expect(targetValidationResult.body.data!.setTargetValidation.enabled).toEqual(true); expect(targetValidationResult.body.data!.setTargetValidation.percentage).toEqual(0); expect(targetValidationResult.body.data!.setTargetValidation.period).toEqual(30); const collectResult = await collect({ operations: [ { timestamp: Date.now(), operation: 'query ping { ping }', operationName: 'ping', fields: ['Query', 'Query.ping'], execution: { ok: true, duration: 200000000, errorsTotal: 0, }, metadata: {}, }, { timestamp: Date.now(), operation: 'query me { me }', operationName: 'me', fields: ['Query', 'Query.me'], execution: { ok: true, duration: 200000000, errorsTotal: 0, }, }, { timestamp: Date.now(), operation: 'query me { me }', operationName: 'me', fields: ['Query', 'Query.me'], execution: { ok: true, duration: 200000000, errorsTotal: 0, }, }, ], token: tokenForProduction, // put collected operation in production }); expect(collectResult.status).toEqual(200); await waitFor(5_000); // should not be breaking because the field is unused on staging const unusedCheckResult = await checkSchema( { sdl: `type Query { me: String }`, // ping is used but on production }, tokenForStaging ); expect(unusedCheckResult.body.errors).not.toBeDefined(); expect(unusedCheckResult.body.data!.schemaCheck.__typename).toEqual('SchemaCheckSuccess'); // Now switch to using checking both staging and production const updateValidationResult = await updateTargetValidationSettings( { organization: org.cleanId, project: project.cleanId, target: staging.cleanId, percentage: 50, // Out of 3 requests, 1 is for Query.me, 2 are done for Query.me so it's 1/3 = 33.3% period: 2, targets: [production.id, staging.id], }, { authToken: owner_access_token, } ); expect(updateValidationResult.body.errors).not.toBeDefined(); expect(updateValidationResult.body.data!.updateTargetValidationSettings.error).toBeNull(); expect( updateValidationResult.body.data!.updateTargetValidationSettings.ok!.updatedTargetValidationSettings.percentage ).toEqual(50); expect( updateValidationResult.body.data!.updateTargetValidationSettings.ok!.updatedTargetValidationSettings.period ).toEqual(2); expect( updateValidationResult.body.data!.updateTargetValidationSettings.ok!.updatedTargetValidationSettings.targets ).toHaveLength(2); // should be non-breaking because the field is used in production and we are checking staging and production now // and it used in less than 50% of traffic const usedCheckResult = await checkSchema( { sdl: `type Query { me: String }`, // ping is used on production and we do check production now }, tokenForStaging ); if (usedCheckResult.body.data!.schemaCheck.__typename !== 'SchemaCheckSuccess') { throw new Error(`Expected SchemaCheckSuccess, got ${usedCheckResult.body.data!.schemaCheck.__typename}`); } expect(usedCheckResult.body.data!.schemaCheck.valid).toEqual(true); expect(usedCheckResult.body.errors).not.toBeDefined(); }); test('check usage not from excluded client names', async () => { const { access_token: owner_access_token } = await authenticate('main'); const orgResult = await createOrganization( { name: 'foo', }, owner_access_token ); const org = orgResult.body.data!.createOrganization.ok!.createdOrganizationPayload.organization; const projectResult = await createProject( { organization: org.cleanId, type: ProjectType.Single, name: 'foo', }, owner_access_token ); const project = projectResult.body.data!.createProject.ok!.createdProject; const production = projectResult.body.data!.createProject.ok!.createdTargets.find(t => t.name === 'production'); if (!production) { throw new Error('No production target'); } const productionTokenResult = await createToken( { name: 'test', organization: org.cleanId, project: project.cleanId, target: production.cleanId, organizationScopes: [OrganizationAccessScope.Read], projectScopes: [ProjectAccessScope.Read], targetScopes: [TargetAccessScope.Read, TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], }, owner_access_token ); expect(productionTokenResult.body.errors).not.toBeDefined(); const tokenForProduction = productionTokenResult.body.data!.createToken.ok!.secret; const schemaPublishResult = await publishSchema( { author: 'Kamil', commit: 'usage-check-2', sdl: `type Query { ping: String me: String }`, }, tokenForProduction ); expect(schemaPublishResult.body.errors).not.toBeDefined(); expect((schemaPublishResult.body.data!.schemaPublish as any).valid).toEqual(true); const targetValidationResult = await setTargetValidation( { enabled: true, organization: org.cleanId, project: project.cleanId, target: production.cleanId, }, { authToken: owner_access_token, } ); expect(targetValidationResult.body.errors).not.toBeDefined(); expect(targetValidationResult.body.data!.setTargetValidation.enabled).toEqual(true); expect(targetValidationResult.body.data!.setTargetValidation.percentage).toEqual(0); expect(targetValidationResult.body.data!.setTargetValidation.period).toEqual(30); const collectResult = await collect({ operations: [ { timestamp: Date.now(), operation: 'query ping { ping }', operationName: 'ping', fields: ['Query', 'Query.ping'], execution: { ok: true, duration: 200000000, errorsTotal: 0, }, metadata: { client: { name: 'cli', version: '2.0.0', }, }, }, { timestamp: Date.now(), operation: 'query me { me }', operationName: 'me', fields: ['Query', 'Query.me'], execution: { ok: true, duration: 200000000, errorsTotal: 0, }, metadata: { client: { name: 'app', version: '1.0.0', }, }, }, { timestamp: Date.now(), operation: 'query me { me }', operationName: 'me', fields: ['Query', 'Query.me'], execution: { ok: true, duration: 200000000, errorsTotal: 0, }, metadata: { client: { name: 'app', version: '1.0.1', }, }, }, ], token: tokenForProduction, }); expect(collectResult.status).toEqual(200); await waitFor(5_000); // should be breaking because the field is used const unusedCheckResult = await checkSchema( { sdl: `type Query { ping: String }`, // Query.me is used }, tokenForProduction ); expect(unusedCheckResult.body.errors).not.toBeDefined(); expect(unusedCheckResult.body.data!.schemaCheck.__typename).toEqual('SchemaCheckError'); // Exclude app from the check const updateValidationResult = await updateTargetValidationSettings( { organization: org.cleanId, project: project.cleanId, target: production.cleanId, percentage: 0, period: 2, targets: [production.id], excludedClients: ['app'], }, { authToken: owner_access_token, } ); expect(updateValidationResult.body.errors).not.toBeDefined(); expect(updateValidationResult.body.data!.updateTargetValidationSettings.error).toBeNull(); expect( updateValidationResult.body.data!.updateTargetValidationSettings.ok!.updatedTargetValidationSettings.enabled ).toBe(true); expect( updateValidationResult.body.data!.updateTargetValidationSettings.ok!.updatedTargetValidationSettings.excludedClients ).toHaveLength(1); expect( updateValidationResult.body.data!.updateTargetValidationSettings.ok!.updatedTargetValidationSettings.excludedClients ).toContainEqual('app'); // should be safe because the field was not used by the non-excluded clients (cli never requested `Query.me`, but app did) const usedCheckResult = await checkSchema( { sdl: `type Query { ping: String }`, }, tokenForProduction ); if (usedCheckResult.body.data!.schemaCheck.__typename !== 'SchemaCheckSuccess') { throw new Error(`Expected SchemaCheckSuccess, got ${usedCheckResult.body.data!.schemaCheck.__typename}`); } expect(usedCheckResult.body.data!.schemaCheck.valid).toEqual(true); expect(usedCheckResult.body.errors).not.toBeDefined(); }); test('number of produced and collected operations should match', async () => { const { access_token: owner_access_token } = await authenticate('main'); const orgResult = await createOrganization( { name: 'foo', }, owner_access_token ); const org = orgResult.body.data!.createOrganization.ok!.createdOrganizationPayload.organization; const projectResult = await createProject( { organization: org.cleanId, type: ProjectType.Single, name: 'foo', }, owner_access_token ); const project = projectResult.body.data!.createProject.ok!.createdProject; const target = projectResult.body.data!.createProject.ok!.createdTargets[0]; const tokenResult = await createToken( { name: 'test', organization: org.cleanId, project: project.cleanId, target: target.cleanId, organizationScopes: [OrganizationAccessScope.Read], projectScopes: [ProjectAccessScope.Read], targetScopes: [TargetAccessScope.Read, TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], }, owner_access_token ); expect(tokenResult.body.errors).not.toBeDefined(); const token = tokenResult.body.data!.createToken.ok!.secret; const batchSize = 1000; const totalAmount = 10_000; for await (const i of new Array(totalAmount / batchSize).fill(null).map((_, i) => i)) { await sendBatch( batchSize, i % 2 === 0 ? { operation: 'query ping { ping }', operationName: 'ping', fields: ['Query', 'Query.ping'], execution: { ok: true, duration: 200000000, errorsTotal: 0, }, } : { operation: 'query ping { ping }', operationName: 'ping', fields: ['Query', 'Query.ping'], execution: { ok: true, duration: 200000000, errorsTotal: 0, }, metadata: { client: { name: 'web', version: '1.2.3', }, }, }, token ); } await waitFor(5_000); const result = await clickHouseQuery<{ target: string; client_name: string | null; hash: string; total: number; }>(` SELECT target, client_name, hash, sum(total) as total FROM ${FF_CLICKHOUSE_V2_TABLES ? 'clients_daily' : 'client_names_daily'} WHERE timestamp >= subtractDays(now(), 30) AND timestamp <= now() GROUP BY target, client_name, hash `); expect(result.rows).toEqual(2); expect(result.data).toContainEqual( expect.objectContaining({ target: target.id, client_name: 'web', hash: expect.any(String), total: expect.stringMatching('5000'), }) ); expect(result.data).toContainEqual( expect.objectContaining({ target: target.id, client_name: '', hash: expect.any(String), total: expect.stringMatching('5000'), }) ); }); test('different order of schema coordinates should not result in different hash', async () => { const { access_token: owner_access_token } = await authenticate('main'); const orgResult = await createOrganization( { name: 'foo', }, owner_access_token ); const org = orgResult.body.data!.createOrganization.ok!.createdOrganizationPayload.organization; const projectResult = await createProject( { organization: org.cleanId, type: ProjectType.Single, name: 'foo', }, owner_access_token ); const project = projectResult.body.data!.createProject.ok!.createdProject; const target = projectResult.body.data!.createProject.ok!.createdTargets[0]; const tokenResult = await createToken( { name: 'test', organization: org.cleanId, project: project.cleanId, target: target.cleanId, organizationScopes: [OrganizationAccessScope.Read], projectScopes: [ProjectAccessScope.Read], targetScopes: [TargetAccessScope.Read, TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], }, owner_access_token ); expect(tokenResult.body.errors).not.toBeDefined(); const token = tokenResult.body.data!.createToken.ok!.secret; await collect({ operations: [ { operation: 'query ping { ping }', // those spaces are expected and important to ensure normalization is in place operationName: 'ping', fields: ['Query', 'Query.ping'], execution: { ok: true, duration: 200000000, errorsTotal: 0, }, }, { operation: 'query ping { ping }', operationName: 'ping', fields: ['Query.ping', 'Query'], execution: { ok: true, duration: 200000000, errorsTotal: 0, }, }, ], token, }); await waitFor(5_000); const coordinatesResult = await clickHouseQuery<{ target: string; client_name: string | null; hash: string; total: number; }>(` SELECT coordinate, hash FROM ${ FF_CLICKHOUSE_V2_TABLES ? 'coordinates_daily' : 'schema_coordinates_daily' } GROUP BY coordinate, hash `); expect(coordinatesResult.rows).toEqual(2); const operationCollectionResult = await clickHouseQuery<{ target: string; client_name: string | null; hash: string; total: number; }>( FF_CLICKHOUSE_V2_TABLES ? `SELECT hash FROM operation_collection GROUP BY hash` : `SELECT hash FROM operations_registry FINAL GROUP BY hash` ); expect(operationCollectionResult.rows).toEqual(1); }); test('same operation but with different schema coordinates should result in different hash', async () => { const { access_token: owner_access_token } = await authenticate('main'); const orgResult = await createOrganization( { name: 'foo', }, owner_access_token ); const org = orgResult.body.data!.createOrganization.ok!.createdOrganizationPayload.organization; const projectResult = await createProject( { organization: org.cleanId, type: ProjectType.Single, name: 'foo', }, owner_access_token ); const project = projectResult.body.data!.createProject.ok!.createdProject; const target = projectResult.body.data!.createProject.ok!.createdTargets[0]; const tokenResult = await createToken( { name: 'test', organization: org.cleanId, project: project.cleanId, target: target.cleanId, organizationScopes: [OrganizationAccessScope.Read], projectScopes: [ProjectAccessScope.Read], targetScopes: [TargetAccessScope.Read, TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], }, owner_access_token ); expect(tokenResult.body.errors).not.toBeDefined(); const token = tokenResult.body.data!.createToken.ok!.secret; await collect({ operations: [ { operation: 'query ping { ping }', // those spaces are expected and important to ensure normalization is in place operationName: 'ping', fields: ['Query', 'Query.ping'], execution: { ok: true, duration: 200000000, errorsTotal: 0, }, }, { operation: 'query ping { ping }', operationName: 'ping', fields: ['RootQuery', 'RootQuery.ping'], execution: { ok: true, duration: 200000000, errorsTotal: 0, }, }, ], token, }); await waitFor(5_000); const coordinatesResult = await clickHouseQuery<{ coordinate: string; hash: string; }>(` SELECT coordinate, hash FROM ${ FF_CLICKHOUSE_V2_TABLES ? 'coordinates_daily' : 'schema_coordinates_daily' } GROUP BY coordinate, hash `); expect(coordinatesResult.rows).toEqual(4); const operationCollectionResult = await clickHouseQuery<{ hash: string; }>( FF_CLICKHOUSE_V2_TABLES ? `SELECT hash FROM operation_collection GROUP BY hash` : `SELECT hash FROM operations_registry FINAL GROUP BY hash` ); expect(operationCollectionResult.rows).toEqual(2); const operationsResult = await clickHouseQuery<{ target: string; client_name: string | null; hash: string; total: number; }>( FF_CLICKHOUSE_V2_TABLES ? `SELECT hash FROM operation_collection GROUP BY hash` : `SELECT hash FROM operations_registry FINAL GROUP BY hash` ); expect(operationsResult.rows).toEqual(2); }); test('operations with the same schema coordinates and body but with different name should result in different hashes', async () => { const { access_token: owner_access_token } = await authenticate('main'); const orgResult = await createOrganization( { name: 'foo', }, owner_access_token ); const org = orgResult.body.data!.createOrganization.ok!.createdOrganizationPayload.organization; const projectResult = await createProject( { organization: org.cleanId, type: ProjectType.Single, name: 'foo', }, owner_access_token ); const project = projectResult.body.data!.createProject.ok!.createdProject; const target = projectResult.body.data!.createProject.ok!.createdTargets[0]; const tokenResult = await createToken( { name: 'test', organization: org.cleanId, project: project.cleanId, target: target.cleanId, organizationScopes: [OrganizationAccessScope.Read], projectScopes: [ProjectAccessScope.Read], targetScopes: [TargetAccessScope.Read, TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], }, owner_access_token ); expect(tokenResult.body.errors).not.toBeDefined(); const token = tokenResult.body.data!.createToken.ok!.secret; await collect({ operations: [ { operation: 'query pingv2 { ping }', operationName: 'pingv2', fields: ['Query', 'Query.ping'], execution: { ok: true, duration: 200000000, errorsTotal: 0, }, }, { operation: 'query ping { ping }', operationName: 'ping', fields: ['Query', 'Query.ping'], execution: { ok: true, duration: 200000000, errorsTotal: 0, }, }, ], token, }); await waitFor(5_000); const coordinatesResult = await clickHouseQuery<{ target: string; client_name: string | null; hash: string; total: number; }>(` SELECT coordinate, hash FROM ${ FF_CLICKHOUSE_V2_TABLES ? 'coordinates_daily' : 'schema_coordinates_daily' } GROUP BY coordinate, hash `); expect(coordinatesResult.rows).toEqual(4); const operationsResult = await clickHouseQuery<{ target: string; client_name: string | null; hash: string; total: number; }>( FF_CLICKHOUSE_V2_TABLES ? `SELECT hash FROM operation_collection GROUP BY hash` : `SELECT hash FROM operations_registry FINAL GROUP BY hash` ); expect(operationsResult.rows).toEqual(2); }); test('ensure correct data', async () => { const { access_token: owner_access_token } = await authenticate('main'); const orgResult = await createOrganization( { name: 'foo', }, owner_access_token ); const org = orgResult.body.data!.createOrganization.ok!.createdOrganizationPayload.organization; const projectResult = await createProject( { organization: org.cleanId, type: ProjectType.Single, name: 'foo', }, owner_access_token ); const project = projectResult.body.data!.createProject.ok!.createdProject; const target = projectResult.body.data!.createProject.ok!.createdTargets[0]; const tokenResult = await createToken( { name: 'test', organization: org.cleanId, project: project.cleanId, target: target.cleanId, organizationScopes: [OrganizationAccessScope.Read], projectScopes: [ProjectAccessScope.Read], targetScopes: [TargetAccessScope.Read, TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], }, owner_access_token ); expect(tokenResult.body.errors).not.toBeDefined(); const token = tokenResult.body.data!.createToken.ok!.secret; await collect({ operations: [ { operation: 'query ping { ping }', // those spaces are expected and important to ensure normalization is in place operationName: 'ping', fields: ['Query', 'Query.ping'], execution: { ok: true, duration: 200000000, errorsTotal: 0, }, }, { operation: 'query ping { ping }', operationName: 'ping', fields: ['Query', 'Query.ping'], execution: { ok: true, duration: 200000000, errorsTotal: 0, }, metadata: { client: { name: 'test-name', version: 'test-version', }, }, }, ], token, }); await waitFor(5_000); if (FF_CLICKHOUSE_V2_TABLES) { // operation_collection const operationCollectionResult = await clickHouseQuery<{ target: string; hash: string; name: string; body: string; operation_kind: string; coordinates: string[]; total: string; timestamp: string; expires_at: string; }>(` SELECT target, hash, name, body, operation_kind, sum(total) as total, coordinates FROM operation_collection GROUP BY target, hash, coordinates, name, body, operation_kind `); expect(operationCollectionResult.data).toHaveLength(1); const operationCollectionRow = operationCollectionResult.data[0]; expect(operationCollectionRow.body).toEqual('query ping{ping}'); expect(operationCollectionRow.coordinates).toHaveLength(2); expect(operationCollectionRow.coordinates).toContainEqual('Query.ping'); expect(operationCollectionRow.coordinates).toContainEqual('Query'); expect(operationCollectionRow.hash).toHaveLength(32); expect(operationCollectionRow.name).toBe('ping'); expect(operationCollectionRow.target).toBe(target.id); expect(ensureNumber(operationCollectionRow.total)).toEqual(2); // operations const operationsResult = await clickHouseQuery<{ target: string; timestamp: string; expires_at: string; hash: string; ok: boolean; errors: number; duration: number; client_name: string; client_version: string; }>(` SELECT target, timestamp, expires_at, hash, ok, errors, duration, client_name, client_version FROM operations `); expect(operationsResult.data).toHaveLength(2); const operationWithClient = operationsResult.data.find(o => o.client_name.length > 0)!; expect(operationWithClient).toBeDefined(); expect(operationWithClient.client_name).toEqual('test-name'); expect(operationWithClient.client_version).toEqual('test-version'); expect(ensureNumber(operationWithClient.duration)).toEqual(200_000_000); expect(ensureNumber(operationWithClient.errors)).toEqual(0); expect(operationWithClient.hash).toHaveLength(32); expect(operationWithClient.target).toEqual(target.id); const operationWithoutClient = operationsResult.data.find(o => o.client_name.length === 0)!; expect(operationWithoutClient).toBeDefined(); expect(operationWithoutClient.client_name).toHaveLength(0); expect(operationWithoutClient.client_version).toHaveLength(0); expect(ensureNumber(operationWithoutClient.duration)).toEqual(200_000_000); expect(ensureNumber(operationWithoutClient.errors)).toEqual(0); expect(operationWithoutClient.hash).toHaveLength(32); expect(operationWithoutClient.target).toEqual(target.id); // operations_hourly const operationsHourlyResult = await clickHouseQuery<{ target: string; hash: string; total_ok: string; total: string; quantiles: [number]; }>(` SELECT target, sum(total) as total, sum(total_ok) as total_ok, hash, quantilesMerge(0.99)(duration_quantiles) as quantiles FROM operations_hourly GROUP BY target, hash `); expect(operationsHourlyResult.data).toHaveLength(1); const hourlyAgg = operationsHourlyResult.data[0]; expect(hourlyAgg).toBeDefined(); expect(ensureNumber(hourlyAgg.quantiles[0])).toEqual(200_000_000); expect(ensureNumber(hourlyAgg.total)).toEqual(2); expect(ensureNumber(hourlyAgg.total_ok)).toEqual(2); expect(hourlyAgg.hash).toHaveLength(32); expect(hourlyAgg.target).toEqual(target.id); // operations_daily const operationsDailyResult = await clickHouseQuery<{ target: string; hash: string; total_ok: string; total: string; quantiles: [number]; }>(` SELECT target, sum(total) as total, sum(total_ok) as total_ok, hash, quantilesMerge(0.99)(duration_quantiles) as quantiles FROM operations_daily GROUP BY target, hash `); expect(operationsDailyResult.data).toHaveLength(1); const dailyAgg = operationsDailyResult.data[0]; expect(dailyAgg).toBeDefined(); expect(ensureNumber(dailyAgg.quantiles[0])).toEqual(200_000_000); expect(ensureNumber(dailyAgg.total)).toEqual(2); expect(ensureNumber(dailyAgg.total_ok)).toEqual(2); expect(dailyAgg.hash).toHaveLength(32); expect(dailyAgg.target).toEqual(target.id); // coordinates_daily const coordinatesDailyResult = await clickHouseQuery<{ target: string; hash: string; total: string; coordinate: string; }>(` SELECT target, sum(total) as total, hash, coordinate FROM coordinates_daily GROUP BY target, hash, coordinate `); expect(coordinatesDailyResult.data).toHaveLength(2); const rootCoordinate = coordinatesDailyResult.data.find(c => c.coordinate === 'Query')!; expect(rootCoordinate).toBeDefined(); expect(ensureNumber(rootCoordinate.total)).toEqual(2); expect(rootCoordinate.hash).toHaveLength(32); expect(rootCoordinate.target).toEqual(target.id); const fieldCoordinate = coordinatesDailyResult.data.find(c => c.coordinate === 'Query.ping')!; expect(fieldCoordinate).toBeDefined(); expect(ensureNumber(fieldCoordinate.total)).toEqual(2); expect(fieldCoordinate.hash).toHaveLength(32); expect(fieldCoordinate.target).toEqual(target.id); // clients_daily const clientsDailyResult = await clickHouseQuery<{ target: string; hash: string; client_name: string; client_version: string; total: string; }>(` SELECT target, sum(total) as total, hash, client_name, client_version FROM clients_daily GROUP BY target, hash, client_name, client_version `); expect(clientsDailyResult.data).toHaveLength(2); const dailyAggOfKnownClient = clientsDailyResult.data.find(c => c.client_name === 'test-name')!; expect(dailyAggOfKnownClient).toBeDefined(); expect(ensureNumber(dailyAggOfKnownClient.total)).toEqual(1); expect(dailyAggOfKnownClient.client_version).toBe('test-version'); expect(dailyAggOfKnownClient.hash).toHaveLength(32); expect(dailyAggOfKnownClient.target).toEqual(target.id); const dailyAggOfUnknownClient = clientsDailyResult.data.find(c => c.client_name !== 'test-name')!; expect(dailyAggOfUnknownClient).toBeDefined(); expect(ensureNumber(dailyAggOfUnknownClient.total)).toEqual(1); expect(dailyAggOfUnknownClient.client_version).toHaveLength(0); expect(dailyAggOfUnknownClient.hash).toHaveLength(32); expect(dailyAggOfUnknownClient.target).toEqual(target.id); } else { // operations_registry const operationsRegistryResult = await clickHouseQuery<{ target: string; hash: string; name: string; body: string; operation: string; }>(` SELECT target, hash, name, body, operation FROM operations_registry FINAL GROUP BY target, hash, name, body, operation `); expect(operationsRegistryResult.data).toHaveLength(1); const operationCollectionRow = operationsRegistryResult.data[0]; expect(operationCollectionRow.body).toEqual('query ping{ping}'); expect(operationCollectionRow.hash).toHaveLength(32); expect(operationCollectionRow.name).toBe('ping'); expect(operationCollectionRow.target).toBe(target.id); // operations_new const operationsResult = await clickHouseQuery<{ target: string; hash: string; ok: boolean; errors: number; duration: number; schema: string[]; client_name: string; client_version: string; }>(` SELECT target, hash, ok, errors, duration, schema, client_name, client_version FROM operations_new `); expect(operationsResult.data).toHaveLength(2); const operationWithClient = operationsResult.data.find(o => o.client_name.length > 0)!; expect(operationWithClient).toBeDefined(); expect(operationWithClient.client_name).toEqual('test-name'); expect(operationWithClient.client_version).toEqual('test-version'); expect(operationWithClient.schema).toHaveLength(2); expect(operationWithClient.schema).toContainEqual('Query.ping'); expect(operationWithClient.schema).toContainEqual('Query'); expect(ensureNumber(operationWithClient.duration)).toEqual(200_000_000); expect(ensureNumber(operationWithClient.errors)).toEqual(0); expect(operationWithClient.hash).toHaveLength(32); expect(operationWithClient.target).toEqual(target.id); const operationWithoutClient = operationsResult.data.find(o => o.client_name.length === 0)!; expect(operationWithoutClient).toBeDefined(); expect(operationWithoutClient.client_name).toHaveLength(0); expect(operationWithoutClient.client_version).toHaveLength(0); expect(operationWithClient.schema).toHaveLength(2); expect(operationWithClient.schema).toContainEqual('Query.ping'); expect(operationWithClient.schema).toContainEqual('Query'); expect(ensureNumber(operationWithoutClient.duration)).toEqual(200_000_000); expect(ensureNumber(operationWithoutClient.errors)).toEqual(0); expect(operationWithoutClient.hash).toHaveLength(32); expect(operationWithoutClient.target).toEqual(target.id); // operations_new_hourly_mv const operationsHourlyResult = await clickHouseQuery<{ target: string; hash: string; total_ok: string; total: string; quantiles: [number]; }>(` SELECT target, hash, sum(total) as total, sum(total_ok) as total_ok, quantilesMerge(0.99)(duration_quantiles) as quantiles FROM operations_new_hourly_mv GROUP BY target, hash `); expect(operationsHourlyResult.data).toHaveLength(1); const hourlyAgg = operationsHourlyResult.data[0]; expect(hourlyAgg).toBeDefined(); expect(ensureNumber(hourlyAgg.quantiles[0])).toEqual(200_000_000); expect(ensureNumber(hourlyAgg.total)).toEqual(2); expect(ensureNumber(hourlyAgg.total_ok)).toEqual(2); expect(hourlyAgg.hash).toHaveLength(32); expect(hourlyAgg.target).toEqual(target.id); // schema_coordinates_daily const coordinatesDailyResult = await clickHouseQuery<{ target: string; hash: string; total: string; coordinate: string; }>(` SELECT target, sum(total) as total, hash, coordinate FROM schema_coordinates_daily GROUP BY target, hash, coordinate `); expect(coordinatesDailyResult.data).toHaveLength(2); const rootCoordinate = coordinatesDailyResult.data.find(c => c.coordinate === 'Query')!; expect(rootCoordinate).toBeDefined(); expect(ensureNumber(rootCoordinate.total)).toEqual(2); expect(rootCoordinate.hash).toHaveLength(32); expect(rootCoordinate.target).toEqual(target.id); const fieldCoordinate = coordinatesDailyResult.data.find(c => c.coordinate === 'Query.ping')!; expect(fieldCoordinate).toBeDefined(); expect(ensureNumber(fieldCoordinate.total)).toEqual(2); expect(fieldCoordinate.hash).toHaveLength(32); expect(fieldCoordinate.target).toEqual(target.id); // clients_daily const clientsDailyResult = await clickHouseQuery<{ target: string; hash: string; client_name: string; total: string; }>(` SELECT target, sum(total) as total, hash, client_name FROM client_names_daily GROUP BY target, hash, client_name `); expect(clientsDailyResult.data).toHaveLength(2); const dailyAggOfKnownClient = clientsDailyResult.data.find(c => c.client_name === 'test-name')!; expect(dailyAggOfKnownClient).toBeDefined(); expect(ensureNumber(dailyAggOfKnownClient.total)).toEqual(1); expect(dailyAggOfKnownClient.hash).toHaveLength(32); expect(dailyAggOfKnownClient.target).toEqual(target.id); const dailyAggOfUnknownClient = clientsDailyResult.data.find(c => c.client_name !== 'test-name')!; expect(dailyAggOfUnknownClient).toBeDefined(); expect(ensureNumber(dailyAggOfUnknownClient.total)).toEqual(1); expect(dailyAggOfUnknownClient.hash).toHaveLength(32); expect(dailyAggOfUnknownClient.target).toEqual(target.id); } });