Skip to content

Commit fdcfc1d

Browse files
authored
feat(api): add OperationStatsValues.name and OperationStatsValues,operationHash to the public api schema (#6813)
1 parent 6bcbbc5 commit fdcfc1d

File tree

13 files changed

+168
-64
lines changed

13 files changed

+168
-64
lines changed

integration-tests/testkit/flow.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1157,7 +1157,7 @@ export function readOperationBody(
11571157
) {
11581158
return execute({
11591159
document: graphql(`
1160-
query readOperationBody($selector: TargetSelectorInput!, $hash: String!) {
1160+
query readOperationBody($selector: TargetSelectorInput!, $hash: ID!) {
11611161
target(reference: { bySelector: $selector }) {
11621162
id
11631163
operation(hash: $hash) {

integration-tests/tests/api/target/usage.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2187,6 +2187,12 @@ const SubscriptionSchemaCheckQuery = graphql(/* GraphQL */ `
21872187
countFormatted
21882188
percentage
21892189
percentageFormatted
2190+
operation {
2191+
hash
2192+
name
2193+
type
2194+
body
2195+
}
21902196
}
21912197
topAffectedClients {
21922198
name
@@ -2897,6 +2903,12 @@ test.concurrent(
28972903
name: 'anonymous',
28982904
percentage: 100,
28992905
percentageFormatted: '100.00%',
2906+
operation: {
2907+
body: 'subscription{a}',
2908+
hash: 'c1bbc8385a4a6f4e4988be7394800adc',
2909+
name: 'anonymous',
2910+
type: 'SUBSCRIPTION',
2911+
},
29002912
},
29012913
]);
29022914
expect(node.usageStatistics?.topAffectedClients).toEqual([

packages/services/api/src/modules/operations/module.graphql.ts

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,9 @@ export default gql`
182182
183183
type OperationStatsValues {
184184
id: ID! @tag(name: "public")
185-
operationHash: String
185+
operationHash: String! @tag(name: "public")
186186
kind: String!
187-
name: String!
187+
name: String! @tag(name: "public")
188188
"""
189189
Total number of requests
190190
"""
@@ -235,26 +235,49 @@ export default gql`
235235
}
236236
237237
enum GraphQLOperationType {
238-
query
239-
mutation
240-
subscription
238+
QUERY @tag(name: "public")
239+
MUTATION @tag(name: "public")
240+
SUBSCRIPTION @tag(name: "public")
241241
}
242242
243243
type Operation {
244-
hash: String!
245-
name: String
246-
type: GraphQLOperationType!
247-
body: String!
244+
"""
245+
Hash that uniquely identifies the operation.
246+
"""
247+
hash: ID! @tag(name: "public")
248+
"""
249+
Name of the operation
250+
"""
251+
name: String @tag(name: "public")
252+
"""
253+
Operation type
254+
"""
255+
type: GraphQLOperationType! @tag(name: "public")
256+
"""
257+
Operation body
258+
"""
259+
body: String! @tag(name: "public")
248260
}
249261
250262
extend type Target {
251263
requestsOverTime(resolution: Int!, period: DateRangeInput!): [RequestsOverTime!]!
252264
totalRequests(period: DateRangeInput!): SafeInt!
253-
operation(hash: String!): Operation
265+
"""
266+
Retrieve an operation via it's hash.
267+
"""
268+
operation(hash: ID! @tag(name: "public")): Operation @tag(name: "public")
254269
}
255270
256271
extend type Project {
257272
requestsOverTime(resolution: Int!, period: DateRangeInput!): [RequestsOverTime!]!
258273
totalRequests(period: DateRangeInput!): SafeInt!
259274
}
275+
276+
extend type SchemaChangeUsageStatisticsAffectedOperation {
277+
"""
278+
Get the associated operation.
279+
The field is nullable as this data is only stored for the duration of the usage retention period.
280+
"""
281+
operation: Operation @tag(name: "public")
282+
}
260283
`;

packages/services/api/src/modules/operations/providers/operations-manager.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -101,24 +101,19 @@ export class OperationsManager {
101101
);
102102
}
103103

104-
async getOperation({
105-
organizationId,
106-
projectId,
107-
targetId,
108-
hash,
109-
}: { hash: string } & TargetSelector) {
104+
async getOperation(args: { hash: string } & TargetSelector) {
110105
await this.session.assertPerformAction({
111106
action: 'project:describe',
112-
organizationId: organizationId,
107+
organizationId: args.organizationId,
113108
params: {
114-
organizationId: organizationId,
115-
projectId: projectId,
109+
organizationId: args.organizationId,
110+
projectId: args.projectId,
116111
},
117112
});
118113

119114
return await this.reader.readOperation({
120-
target: targetId,
121-
hash,
115+
targetIds: [args.targetId],
116+
hash: args.hash,
122117
});
123118
}
124119

packages/services/api/src/modules/operations/providers/operations-reader.ts

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ export interface DurationMetrics {
4040
p99: number;
4141
}
4242

43+
const ReadOperationModel = z.object({
44+
target: z.string().uuid(),
45+
hash: z.string(),
46+
body: z.string(),
47+
name: z.string(),
48+
type: z.union([z.literal('QUERY'), z.literal('MUTATION'), z.literal('SUBSCRIPTION')]),
49+
});
50+
51+
type Operation = z.TypeOf<typeof ReadOperationModel>;
52+
4353
function toDurationMetrics(percentiles: [number, number, number, number], avg: number) {
4454
return {
4555
avg,
@@ -684,47 +694,70 @@ export class OperationsReader {
684694
});
685695
}
686696

687-
async readOperation({ target, hash }: { target: string; hash: string }) {
688-
const result = await this.clickHouse.query<{
689-
hash: string;
690-
body: string;
691-
name: string;
692-
type: 'query' | 'mutation' | 'subscription';
693-
}>({
694-
query: sql`
697+
readOperation = batch<{ targetIds: Array<string>; hash: string }, Operation | null>(
698+
async args => {
699+
const allTargetIds = Array.from(new Set(args.flatMap(arg => arg.targetIds)));
700+
const hashes = Array.from(new Set(args.map(arg => arg.hash)));
701+
702+
const result = await this.clickHouse.query<unknown>({
703+
query: sql`
695704
SELECT
705+
"operation_collection_details"."target" AS "target",
696706
"operation_collection_details"."hash" AS "hash",
697-
"operation_collection_details"."operation_kind" AS "type",
707+
upper("operation_collection_details"."operation_kind") AS "type",
698708
"operation_collection_details"."name" AS "name",
699709
"body_join"."body" AS "body"
700710
FROM "operation_collection_details"
701711
RIGHT JOIN (
702712
SELECT
713+
"operation_collection_body"."target" AS "target",
703714
"operation_collection_body"."hash" AS "hash",
704715
"operation_collection_body"."body" AS "body"
705716
FROM "operation_collection_body"
706717
${this.createFilter({
707-
target,
708-
extra: [sql`"operation_collection_body"."hash" = ${hash}`],
718+
target: allTargetIds,
719+
extra: [sql`"operation_collection_body"."hash" IN (${sql.array(hashes, 'String')})`],
709720
namespace: 'operation_collection_body',
710721
})}
711722
LIMIT 1
723+
BY
724+
"operation_collection_body"."target",
725+
"operation_collection_body"."hash"
712726
) AS "body_join"
713727
ON "operation_collection_details"."hash" = "body_join"."hash"
714728
${this.createFilter({
715-
target,
716-
extra: [sql`"operation_collection_details"."hash" = ${hash}`],
729+
target: allTargetIds,
730+
extra: [sql`"operation_collection_details"."hash" IN (${sql.array(hashes, 'String')})`],
717731
namespace: 'operation_collection_details',
718732
})}
719733
LIMIT 1
734+
BY
735+
"operation_collection_details"."target" AS "target",
736+
"operation_collection_details"."hash"
720737
SETTINGS allow_asynchronous_read_from_io_pool_for_merge_tree = 1
721738
`,
722-
queryId: 'read_body',
723-
timeout: 10_000,
724-
});
739+
queryId: 'read_body',
740+
timeout: 10_000,
741+
});
725742

726-
return result.data.length ? result.data[0] : null;
727-
}
743+
const lookupMap = new Map<string, Operation>(
744+
result.data.map(row => {
745+
const record = ReadOperationModel.parse(row);
746+
return [`${record.target}/${record.hash}`, record];
747+
}),
748+
);
749+
750+
return args.map(async arg => {
751+
for (const targetId of arg.targetIds) {
752+
const operation = lookupMap.get(`${targetId}/${arg.hash}`);
753+
if (operation) {
754+
return operation;
755+
}
756+
}
757+
return null;
758+
});
759+
},
760+
);
728761

729762
async getReportedSchemaCoordinates({
730763
target,
@@ -1044,7 +1077,7 @@ export class OperationsReader {
10441077
SELECT
10451078
"operation_collection_details"."name",
10461079
"operation_collection_details"."hash"
1047-
FROM
1080+
FROM
10481081
"operation_collection_details"
10491082
PREWHERE
10501083
"operation_collection_details"."target" IN (${sql.array(args.targetIds, 'String')})
@@ -1622,7 +1655,7 @@ export class OperationsReader {
16221655
FROM ${aggregationTableName('operations')}
16231656
${this.createFilter({ target: targets, period: roundedPeriod })}
16241657
GROUP BY target, date
1625-
ORDER BY
1658+
ORDER BY
16261659
target,
16271660
date
16281661
WITH FILL
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { SchemaChangeUsageStatisticsAffectedOperationResolvers } from '../../../__generated__/types';
2+
import { OperationsReader } from '../providers/operations-reader';
3+
4+
export const SchemaChangeUsageStatisticsAffectedOperation: Pick<
5+
SchemaChangeUsageStatisticsAffectedOperationResolvers,
6+
'operation' | '__isTypeOf'
7+
> = {
8+
operation: (affectedOperation, _, { injector }) => {
9+
return injector.get(OperationsReader).readOperation({
10+
targetIds: affectedOperation.targetIds,
11+
hash: affectedOperation.hash,
12+
});
13+
},
14+
};

packages/services/api/src/modules/organization/lib/organization-access-token-permissions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export const permissionGroups: Array<PermissionGroup> = [
7575
},
7676
{
7777
id: 'target:delete',
78-
title: 'Create target',
78+
title: 'Delete target',
7979
description: 'Delete targets',
8080
},
8181
{

packages/services/api/src/modules/schema/module.graphql.mappers.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,3 +302,11 @@ export type SchemaMetadataMapper = {
302302
content: string;
303303
source: string | null;
304304
};
305+
306+
export type SchemaChangeUsageStatisticsAffectedOperationMapper = {
307+
name: string;
308+
hash: string;
309+
count: number;
310+
percentage: number;
311+
targetIds: Array<string>;
312+
};

packages/services/api/src/modules/schema/providers/breaking-schema-changes-helper.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,41 +11,40 @@ import { formatNumber, formatPercentage } from '../lib/number-formatting';
1111
export class BreakingSchemaChangeUsageHelper {
1212
constructor() {}
1313

14-
private breakingSchemaChangeToUsageMap = new WeakMap<
14+
private breakingSchemaChangeToMetadataMap = new WeakMap<
1515
SchemaChangeType,
16-
ConditionalBreakingChangeMetadata['usage']
16+
ConditionalBreakingChangeMetadata
1717
>();
1818

19-
registerUsageDataForBreakingSchemaChange(
19+
registerMetadataForBreakingSchemaChange(
2020
schemaChange: SchemaChangeType,
21-
usage: ConditionalBreakingChangeMetadata['usage'],
21+
metadata: ConditionalBreakingChangeMetadata,
2222
) {
23-
this.breakingSchemaChangeToUsageMap.set(schemaChange, usage);
23+
this.breakingSchemaChangeToMetadataMap.set(schemaChange, metadata);
2424
}
2525

2626
async getUsageDataForBreakingSchemaChange(schemaChange: SchemaChangeType) {
2727
if (schemaChange.usageStatistics === null) {
2828
return null;
2929
}
3030

31-
const usageData = this.breakingSchemaChangeToUsageMap.get(schemaChange);
31+
const metadata = this.breakingSchemaChangeToMetadataMap.get(schemaChange);
3232

33-
if (usageData == null) {
33+
if (metadata == null) {
3434
return null;
3535
}
3636

3737
return {
3838
topAffectedOperations: schemaChange.usageStatistics.topAffectedOperations.map(operation => {
39-
const percentage = (operation.count / usageData.totalRequestCount) * 100;
39+
const percentage = (operation.count / metadata.usage.totalRequestCount) * 100;
4040
return {
4141
...operation,
42-
countFormatted: formatNumber(operation.count),
4342
percentage,
44-
percentageFormatted: formatPercentage(percentage),
43+
targetIds: metadata.settings.targets.map(target => target.id),
4544
};
4645
}),
4746
topAffectedClients: schemaChange.usageStatistics.topAffectedClients.map(client => {
48-
const percentage = (client.count / usageData.totalRequestCount) * 100;
47+
const percentage = (client.count / metadata.usage.totalRequestCount) * 100;
4948

5049
return {
5150
...client,

packages/services/api/src/modules/schema/providers/contracts-manager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,9 +253,9 @@ export class ContractsManager {
253253
for (const edge of contractChecks.edges) {
254254
if (edge.node.breakingSchemaChanges) {
255255
for (const breakingSchemaChange of edge.node.breakingSchemaChanges) {
256-
this.breakingSchemaChangeUsageHelper.registerUsageDataForBreakingSchemaChange(
256+
this.breakingSchemaChangeUsageHelper.registerMetadataForBreakingSchemaChange(
257257
breakingSchemaChange,
258-
schemaCheck.conditionalBreakingChangeMetadata.usage,
258+
schemaCheck.conditionalBreakingChangeMetadata,
259259
);
260260
}
261261
}

0 commit comments

Comments
 (0)