diff --git a/package.json b/package.json index 7ae3da5..77ac11f 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "lint:fix": "pnpm run -r --parallel lint:fix", "clean": "del-cli '**/node_modules/' '**/**/dist/' '**/**/gen/' '**/**/.next' '**/**/tsconfig.tsbuildinfo' '**/**/.eslintcache'", "release-preview": "lerna publish --ignore-scripts --dry-run", - "release": "lerna publish -y" + "release": "lerna publish -y", + "format": "pnpm -r run format" }, "devDependencies": { "@commitlint/cli": "19.2.1", diff --git a/packages/apollo-to-cosmo-metrics/README.md b/packages/apollo-to-cosmo-metrics/README.md index 0ac448f..98116dd 100644 --- a/packages/apollo-to-cosmo-metrics/README.md +++ b/packages/apollo-to-cosmo-metrics/README.md @@ -33,18 +33,16 @@ const gateway = new ApolloGateway({ // Plugin definition const cosmoReportPlugin = cosmoReportPlugin( - new CosmoClient({ - endpointUrl: 'https://cosmo-metrics.wundergraph.com', - routerToken: 'router-token', - }), - ); + new CosmoClient({ + endpointUrl: 'https://cosmo-metrics.wundergraph.com', + routerToken: 'router-token', + }), +); const server = new ApolloServer({ gateway, plugins: [cosmoReportPlugin], }); -startStandaloneServer(server) - +startStandaloneServer(server); ``` - diff --git a/packages/apollo-to-cosmo-metrics/package.json b/packages/apollo-to-cosmo-metrics/package.json index ef7d43c..37cf8eb 100644 --- a/packages/apollo-to-cosmo-metrics/package.json +++ b/packages/apollo-to-cosmo-metrics/package.json @@ -1,51 +1,52 @@ { - "name": "@wundergraph/apollo-to-cosmo-metrics", - "description": "An apollo gateway plugin that exports schema usage metrics to cosmo", - "version": "0.1.1", - "author": { - "name": "WunderGraph Maintainers", - "email": "info@wundergraph.com" - }, - "license": "Apache-2.0", - "publishConfig": { - "access": "public" - }, - "repository": { - "url": "https://github.com/wundergraph/apollo-compatibility" - }, - "keywords": [ - "wundergraph", - "cosmo", - "gateway", - "apollo", - "federation", - "graphql" - ], - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "type": "module", - "files": [ - "dist" - ], - "scripts": { - "start": "node ./dist/index.js", - "build": "del dist && tsc", - "test": "tsc -p tsconfig.test.json && vitest run --reporter=default --reporter=hanging-process" - }, - "dependencies": { - "@apollo/server": "^4.11.0", - "@bufbuild/protobuf": "^1.9.0", - "@connectrpc/connect": "^1.5.0", - "@connectrpc/connect-node": "^1.5.0", - "@esm2cjs/yocto-queue": "^1.0.0", - "@wundergraph/composition": "^0.29.0", - "graphql": "^16.9.0" - }, - "devDependencies": { - "@types/lodash-es": "^4.17.12", - "@types/node": "^22.7.4", - "lodash-es": "^4.17.21", - "typescript": "^5.6.2", - "vitest": "^2.1.3" - } + "name": "@wundergraph/apollo-to-cosmo-metrics", + "description": "An apollo gateway plugin that exports schema usage metrics to cosmo", + "version": "0.1.1", + "author": { + "name": "WunderGraph Maintainers", + "email": "info@wundergraph.com" + }, + "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, + "repository": { + "url": "https://github.com/wundergraph/apollo-compatibility" + }, + "keywords": [ + "wundergraph", + "cosmo", + "gateway", + "apollo", + "federation", + "graphql" + ], + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "type": "module", + "files": [ + "dist" + ], + "scripts": { + "start": "node ./dist/index.js", + "build": "del dist && tsc", + "test": "tsc -p tsconfig.test.json && vitest run --reporter=default --reporter=hanging-process", + "format": "prettier --write -c src" + }, + "dependencies": { + "@apollo/server": "^4.11.0", + "@bufbuild/protobuf": "^1.9.0", + "@connectrpc/connect": "^1.5.0", + "@connectrpc/connect-node": "^1.5.0", + "@esm2cjs/yocto-queue": "^1.0.0", + "@wundergraph/composition": "^0.29.0", + "graphql": "^16.9.0" + }, + "devDependencies": { + "@types/lodash-es": "^4.17.12", + "@types/node": "^22.7.4", + "lodash-es": "^4.17.21", + "typescript": "^5.6.2", + "vitest": "^2.1.3" + } } diff --git a/packages/apollo-to-cosmo-metrics/src/generated/graphqlmetrics/v1/graphqlmetrics-GraphQLMetricsService_connectquery.ts b/packages/apollo-to-cosmo-metrics/src/generated/graphqlmetrics/v1/graphqlmetrics-GraphQLMetricsService_connectquery.ts index bfd2418..d10c893 100644 --- a/packages/apollo-to-cosmo-metrics/src/generated/graphqlmetrics/v1/graphqlmetrics-GraphQLMetricsService_connectquery.ts +++ b/packages/apollo-to-cosmo-metrics/src/generated/graphqlmetrics/v1/graphqlmetrics-GraphQLMetricsService_connectquery.ts @@ -5,8 +5,13 @@ /* eslint-disable */ // @ts-nocheck -import { MethodKind } from "@bufbuild/protobuf"; -import { PublishAggregatedGraphQLRequestMetricsRequest, PublishAggregatedGraphQLRequestMetricsResponse, PublishGraphQLRequestMetricsRequest, PublishOperationCoverageReportResponse } from "./graphqlmetrics_pb.js"; +import { MethodKind } from '@bufbuild/protobuf'; +import { + PublishAggregatedGraphQLRequestMetricsRequest, + PublishAggregatedGraphQLRequestMetricsResponse, + PublishGraphQLRequestMetricsRequest, + PublishOperationCoverageReportResponse, +} from './graphqlmetrics_pb.js'; /** * PublishGraphQLMetrics publishes the GraphQL metrics to the metrics service @@ -14,26 +19,26 @@ import { PublishAggregatedGraphQLRequestMetricsRequest, PublishAggregatedGraphQL * @generated from rpc wg.cosmo.graphqlmetrics.v1.GraphQLMetricsService.PublishGraphQLMetrics */ export const publishGraphQLMetrics = { - localName: "publishGraphQLMetrics", - name: "PublishGraphQLMetrics", + localName: 'publishGraphQLMetrics', + name: 'PublishGraphQLMetrics', kind: MethodKind.Unary, I: PublishGraphQLRequestMetricsRequest, O: PublishOperationCoverageReportResponse, service: { - typeName: "wg.cosmo.graphqlmetrics.v1.GraphQLMetricsService" - } + typeName: 'wg.cosmo.graphqlmetrics.v1.GraphQLMetricsService', + }, } as const; /** * @generated from rpc wg.cosmo.graphqlmetrics.v1.GraphQLMetricsService.PublishAggregatedGraphQLMetrics */ export const publishAggregatedGraphQLMetrics = { - localName: "publishAggregatedGraphQLMetrics", - name: "PublishAggregatedGraphQLMetrics", + localName: 'publishAggregatedGraphQLMetrics', + name: 'PublishAggregatedGraphQLMetrics', kind: MethodKind.Unary, I: PublishAggregatedGraphQLRequestMetricsRequest, O: PublishAggregatedGraphQLRequestMetricsResponse, service: { - typeName: "wg.cosmo.graphqlmetrics.v1.GraphQLMetricsService" - } + typeName: 'wg.cosmo.graphqlmetrics.v1.GraphQLMetricsService', + }, } as const; diff --git a/packages/apollo-to-cosmo-metrics/src/generated/graphqlmetrics/v1/graphqlmetrics_connect.ts b/packages/apollo-to-cosmo-metrics/src/generated/graphqlmetrics/v1/graphqlmetrics_connect.ts index cf59307..5f9b7b2 100644 --- a/packages/apollo-to-cosmo-metrics/src/generated/graphqlmetrics/v1/graphqlmetrics_connect.ts +++ b/packages/apollo-to-cosmo-metrics/src/generated/graphqlmetrics/v1/graphqlmetrics_connect.ts @@ -5,14 +5,19 @@ /* eslint-disable */ // @ts-nocheck -import { PublishAggregatedGraphQLRequestMetricsRequest, PublishAggregatedGraphQLRequestMetricsResponse, PublishGraphQLRequestMetricsRequest, PublishOperationCoverageReportResponse } from "./graphqlmetrics_pb.js"; -import { MethodKind } from "@bufbuild/protobuf"; +import { + PublishAggregatedGraphQLRequestMetricsRequest, + PublishAggregatedGraphQLRequestMetricsResponse, + PublishGraphQLRequestMetricsRequest, + PublishOperationCoverageReportResponse, +} from './graphqlmetrics_pb.js'; +import { MethodKind } from '@bufbuild/protobuf'; /** * @generated from service wg.cosmo.graphqlmetrics.v1.GraphQLMetricsService */ export const GraphQLMetricsService = { - typeName: "wg.cosmo.graphqlmetrics.v1.GraphQLMetricsService", + typeName: 'wg.cosmo.graphqlmetrics.v1.GraphQLMetricsService', methods: { /** * PublishGraphQLMetrics publishes the GraphQL metrics to the metrics service @@ -20,7 +25,7 @@ export const GraphQLMetricsService = { * @generated from rpc wg.cosmo.graphqlmetrics.v1.GraphQLMetricsService.PublishGraphQLMetrics */ publishGraphQLMetrics: { - name: "PublishGraphQLMetrics", + name: 'PublishGraphQLMetrics', I: PublishGraphQLRequestMetricsRequest, O: PublishOperationCoverageReportResponse, kind: MethodKind.Unary, @@ -29,11 +34,10 @@ export const GraphQLMetricsService = { * @generated from rpc wg.cosmo.graphqlmetrics.v1.GraphQLMetricsService.PublishAggregatedGraphQLMetrics */ publishAggregatedGraphQLMetrics: { - name: "PublishAggregatedGraphQLMetrics", + name: 'PublishAggregatedGraphQLMetrics', I: PublishAggregatedGraphQLRequestMetricsRequest, O: PublishAggregatedGraphQLRequestMetricsResponse, kind: MethodKind.Unary, }, - } + }, } as const; - diff --git a/packages/apollo-to-cosmo-metrics/src/generated/graphqlmetrics/v1/graphqlmetrics_pb.ts b/packages/apollo-to-cosmo-metrics/src/generated/graphqlmetrics/v1/graphqlmetrics_pb.ts index 1e3e5b2..7572099 100644 --- a/packages/apollo-to-cosmo-metrics/src/generated/graphqlmetrics/v1/graphqlmetrics_pb.ts +++ b/packages/apollo-to-cosmo-metrics/src/generated/graphqlmetrics/v1/graphqlmetrics_pb.ts @@ -5,8 +5,15 @@ /* eslint-disable */ // @ts-nocheck -import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf"; -import { Message, proto3, protoInt64 } from "@bufbuild/protobuf"; +import type { + BinaryReadOptions, + FieldList, + JsonReadOptions, + JsonValue, + PartialMessage, + PlainMessage, +} from '@bufbuild/protobuf'; +import { Message, proto3, protoInt64 } from '@bufbuild/protobuf'; /** * @generated from enum wg.cosmo.graphqlmetrics.v1.OperationType @@ -28,10 +35,10 @@ export enum OperationType { SUBSCRIPTION = 2, } // Retrieve enum metadata with: proto3.getEnumType(OperationType) -proto3.util.setEnumType(OperationType, "wg.cosmo.graphqlmetrics.v1.OperationType", [ - { no: 0, name: "QUERY" }, - { no: 1, name: "MUTATION" }, - { no: 2, name: "SUBSCRIPTION" }, +proto3.util.setEnumType(OperationType, 'wg.cosmo.graphqlmetrics.v1.OperationType', [ + { no: 0, name: 'QUERY' }, + { no: 1, name: 'MUTATION' }, + { no: 2, name: 'SUBSCRIPTION' }, ]); /** @@ -54,10 +61,10 @@ export class RequestInfo extends Message { } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "wg.cosmo.graphqlmetrics.v1.RequestInfo"; + static readonly typeName = 'wg.cosmo.graphqlmetrics.v1.RequestInfo'; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "StatusCode", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, - { no: 2, name: "error", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, + { no: 1, name: 'StatusCode', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + { no: 2, name: 'error', kind: 'scalar', T: 8 /* ScalarType.BOOL */ }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): RequestInfo { @@ -72,7 +79,10 @@ export class RequestInfo extends Message { return new RequestInfo().fromJsonString(jsonString, options); } - static equals(a: RequestInfo | PlainMessage | undefined, b: RequestInfo | PlainMessage | undefined): boolean { + static equals( + a: RequestInfo | PlainMessage | undefined, + b: RequestInfo | PlainMessage | undefined, + ): boolean { return proto3.util.equals(RequestInfo, a, b); } } @@ -86,7 +96,7 @@ export class SchemaUsageInfo extends Message { * * @generated from field: string RequestDocument = 1; */ - RequestDocument = ""; + RequestDocument = ''; /** * TypeFieldMetrics is the list of used fields in the request document @@ -150,17 +160,23 @@ export class SchemaUsageInfo extends Message { } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "wg.cosmo.graphqlmetrics.v1.SchemaUsageInfo"; + static readonly typeName = 'wg.cosmo.graphqlmetrics.v1.SchemaUsageInfo'; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "RequestDocument", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 2, name: "TypeFieldMetrics", kind: "message", T: TypeFieldUsageInfo, repeated: true }, - { no: 3, name: "OperationInfo", kind: "message", T: OperationInfo }, - { no: 4, name: "SchemaInfo", kind: "message", T: SchemaInfo }, - { no: 5, name: "ClientInfo", kind: "message", T: ClientInfo }, - { no: 6, name: "RequestInfo", kind: "message", T: RequestInfo }, - { no: 7, name: "Attributes", kind: "map", K: 9 /* ScalarType.STRING */, V: {kind: "scalar", T: 9 /* ScalarType.STRING */} }, - { no: 8, name: "ArgumentMetrics", kind: "message", T: ArgumentUsageInfo, repeated: true }, - { no: 9, name: "InputMetrics", kind: "message", T: InputUsageInfo, repeated: true }, + { no: 1, name: 'RequestDocument', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'TypeFieldMetrics', kind: 'message', T: TypeFieldUsageInfo, repeated: true }, + { no: 3, name: 'OperationInfo', kind: 'message', T: OperationInfo }, + { no: 4, name: 'SchemaInfo', kind: 'message', T: SchemaInfo }, + { no: 5, name: 'ClientInfo', kind: 'message', T: ClientInfo }, + { no: 6, name: 'RequestInfo', kind: 'message', T: RequestInfo }, + { + no: 7, + name: 'Attributes', + kind: 'map', + K: 9 /* ScalarType.STRING */, + V: { kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + }, + { no: 8, name: 'ArgumentMetrics', kind: 'message', T: ArgumentUsageInfo, repeated: true }, + { no: 9, name: 'InputMetrics', kind: 'message', T: InputUsageInfo, repeated: true }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): SchemaUsageInfo { @@ -175,7 +191,10 @@ export class SchemaUsageInfo extends Message { return new SchemaUsageInfo().fromJsonString(jsonString, options); } - static equals(a: SchemaUsageInfo | PlainMessage | undefined, b: SchemaUsageInfo | PlainMessage | undefined): boolean { + static equals( + a: SchemaUsageInfo | PlainMessage | undefined, + b: SchemaUsageInfo | PlainMessage | undefined, + ): boolean { return proto3.util.equals(SchemaUsageInfo, a, b); } } @@ -200,10 +219,10 @@ export class SchemaUsageInfoAggregation extends Message [ - { no: 1, name: "SchemaUsage", kind: "message", T: SchemaUsageInfo }, - { no: 2, name: "RequestCount", kind: "scalar", T: 4 /* ScalarType.UINT64 */ }, + { no: 1, name: 'SchemaUsage', kind: 'message', T: SchemaUsageInfo }, + { no: 2, name: 'RequestCount', kind: 'scalar', T: 4 /* ScalarType.UINT64 */ }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): SchemaUsageInfoAggregation { @@ -218,7 +237,10 @@ export class SchemaUsageInfoAggregation extends Message | undefined, b: SchemaUsageInfoAggregation | PlainMessage | undefined): boolean { + static equals( + a: SchemaUsageInfoAggregation | PlainMessage | undefined, + b: SchemaUsageInfoAggregation | PlainMessage | undefined, + ): boolean { return proto3.util.equals(SchemaUsageInfoAggregation, a, b); } } @@ -232,14 +254,14 @@ export class ClientInfo extends Message { * * @generated from field: string Name = 1; */ - Name = ""; + Name = ''; /** * Version is the GraphQL client version obtained from the request header * * @generated from field: string Version = 2; */ - Version = ""; + Version = ''; constructor(data?: PartialMessage) { super(); @@ -247,10 +269,10 @@ export class ClientInfo extends Message { } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "wg.cosmo.graphqlmetrics.v1.ClientInfo"; + static readonly typeName = 'wg.cosmo.graphqlmetrics.v1.ClientInfo'; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "Name", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 2, name: "Version", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 1, name: 'Name', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'Version', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): ClientInfo { @@ -265,7 +287,10 @@ export class ClientInfo extends Message { return new ClientInfo().fromJsonString(jsonString, options); } - static equals(a: ClientInfo | PlainMessage | undefined, b: ClientInfo | PlainMessage | undefined): boolean { + static equals( + a: ClientInfo | PlainMessage | undefined, + b: ClientInfo | PlainMessage | undefined, + ): boolean { return proto3.util.equals(ClientInfo, a, b); } } @@ -279,14 +304,14 @@ export class OperationInfo extends Message { * * @generated from field: string Hash = 1; */ - Hash = ""; + Hash = ''; /** * Name is the operation name * * @generated from field: string Name = 2; */ - Name = ""; + Name = ''; /** * Type is the operation type @@ -301,11 +326,11 @@ export class OperationInfo extends Message { } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "wg.cosmo.graphqlmetrics.v1.OperationInfo"; + static readonly typeName = 'wg.cosmo.graphqlmetrics.v1.OperationInfo'; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "Hash", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 2, name: "Name", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 3, name: "Type", kind: "enum", T: proto3.getEnumType(OperationType) }, + { no: 1, name: 'Hash', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'Name', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 3, name: 'Type', kind: 'enum', T: proto3.getEnumType(OperationType) }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): OperationInfo { @@ -320,7 +345,10 @@ export class OperationInfo extends Message { return new OperationInfo().fromJsonString(jsonString, options); } - static equals(a: OperationInfo | PlainMessage | undefined, b: OperationInfo | PlainMessage | undefined): boolean { + static equals( + a: OperationInfo | PlainMessage | undefined, + b: OperationInfo | PlainMessage | undefined, + ): boolean { return proto3.util.equals(OperationInfo, a, b); } } @@ -336,7 +364,7 @@ export class SchemaInfo extends Message { * * @generated from field: string Version = 3; */ - Version = ""; + Version = ''; constructor(data?: PartialMessage) { super(); @@ -344,9 +372,9 @@ export class SchemaInfo extends Message { } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "wg.cosmo.graphqlmetrics.v1.SchemaInfo"; + static readonly typeName = 'wg.cosmo.graphqlmetrics.v1.SchemaInfo'; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 3, name: "Version", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 3, name: 'Version', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): SchemaInfo { @@ -361,7 +389,10 @@ export class SchemaInfo extends Message { return new SchemaInfo().fromJsonString(jsonString, options); } - static equals(a: SchemaInfo | PlainMessage | undefined, b: SchemaInfo | PlainMessage | undefined): boolean { + static equals( + a: SchemaInfo | PlainMessage | undefined, + b: SchemaInfo | PlainMessage | undefined, + ): boolean { return proto3.util.equals(SchemaInfo, a, b); } } @@ -403,7 +434,7 @@ export class TypeFieldUsageInfo extends Message { * * @generated from field: string NamedType = 5; */ - NamedType = ""; + NamedType = ''; /** * IndirectInterfaceField is true if the field is an interface field that is used through an implementing type @@ -418,14 +449,14 @@ export class TypeFieldUsageInfo extends Message { } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "wg.cosmo.graphqlmetrics.v1.TypeFieldUsageInfo"; + static readonly typeName = 'wg.cosmo.graphqlmetrics.v1.TypeFieldUsageInfo'; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "Path", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, - { no: 2, name: "TypeNames", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, - { no: 3, name: "SubgraphIDs", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, - { no: 4, name: "Count", kind: "scalar", T: 4 /* ScalarType.UINT64 */ }, - { no: 5, name: "NamedType", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 6, name: "IndirectInterfaceField", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, + { no: 1, name: 'Path', kind: 'scalar', T: 9 /* ScalarType.STRING */, repeated: true }, + { no: 2, name: 'TypeNames', kind: 'scalar', T: 9 /* ScalarType.STRING */, repeated: true }, + { no: 3, name: 'SubgraphIDs', kind: 'scalar', T: 9 /* ScalarType.STRING */, repeated: true }, + { no: 4, name: 'Count', kind: 'scalar', T: 4 /* ScalarType.UINT64 */ }, + { no: 5, name: 'NamedType', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 6, name: 'IndirectInterfaceField', kind: 'scalar', T: 8 /* ScalarType.BOOL */ }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): TypeFieldUsageInfo { @@ -440,7 +471,10 @@ export class TypeFieldUsageInfo extends Message { return new TypeFieldUsageInfo().fromJsonString(jsonString, options); } - static equals(a: TypeFieldUsageInfo | PlainMessage | undefined, b: TypeFieldUsageInfo | PlainMessage | undefined): boolean { + static equals( + a: TypeFieldUsageInfo | PlainMessage | undefined, + b: TypeFieldUsageInfo | PlainMessage | undefined, + ): boolean { return proto3.util.equals(TypeFieldUsageInfo, a, b); } } @@ -461,7 +495,7 @@ export class ArgumentUsageInfo extends Message { * * @generated from field: string TypeName = 2; */ - TypeName = ""; + TypeName = ''; /** * Count is the number of times the argument is used. Useful for batching at client side. @@ -475,7 +509,7 @@ export class ArgumentUsageInfo extends Message { * * @generated from field: string NamedType = 4; */ - NamedType = ""; + NamedType = ''; constructor(data?: PartialMessage) { super(); @@ -483,12 +517,12 @@ export class ArgumentUsageInfo extends Message { } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "wg.cosmo.graphqlmetrics.v1.ArgumentUsageInfo"; + static readonly typeName = 'wg.cosmo.graphqlmetrics.v1.ArgumentUsageInfo'; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "Path", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, - { no: 2, name: "TypeName", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 3, name: "Count", kind: "scalar", T: 4 /* ScalarType.UINT64 */ }, - { no: 4, name: "NamedType", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 1, name: 'Path', kind: 'scalar', T: 9 /* ScalarType.STRING */, repeated: true }, + { no: 2, name: 'TypeName', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 3, name: 'Count', kind: 'scalar', T: 4 /* ScalarType.UINT64 */ }, + { no: 4, name: 'NamedType', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): ArgumentUsageInfo { @@ -503,7 +537,10 @@ export class ArgumentUsageInfo extends Message { return new ArgumentUsageInfo().fromJsonString(jsonString, options); } - static equals(a: ArgumentUsageInfo | PlainMessage | undefined, b: ArgumentUsageInfo | PlainMessage | undefined): boolean { + static equals( + a: ArgumentUsageInfo | PlainMessage | undefined, + b: ArgumentUsageInfo | PlainMessage | undefined, + ): boolean { return proto3.util.equals(ArgumentUsageInfo, a, b); } } @@ -524,7 +561,7 @@ export class InputUsageInfo extends Message { * * @generated from field: string TypeName = 2; */ - TypeName = ""; + TypeName = ''; /** * Count is the number of times the argument is used. Useful for batching at client side. @@ -538,7 +575,7 @@ export class InputUsageInfo extends Message { * * @generated from field: string NamedType = 4; */ - NamedType = ""; + NamedType = ''; /** * EnumValues is an empty list if the input field is not an enum, otherwise it contains the list of used enum values @@ -553,13 +590,13 @@ export class InputUsageInfo extends Message { } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "wg.cosmo.graphqlmetrics.v1.InputUsageInfo"; + static readonly typeName = 'wg.cosmo.graphqlmetrics.v1.InputUsageInfo'; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "Path", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, - { no: 2, name: "TypeName", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 3, name: "Count", kind: "scalar", T: 4 /* ScalarType.UINT64 */ }, - { no: 4, name: "NamedType", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 5, name: "EnumValues", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + { no: 1, name: 'Path', kind: 'scalar', T: 9 /* ScalarType.STRING */, repeated: true }, + { no: 2, name: 'TypeName', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 3, name: 'Count', kind: 'scalar', T: 4 /* ScalarType.UINT64 */ }, + { no: 4, name: 'NamedType', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 5, name: 'EnumValues', kind: 'scalar', T: 9 /* ScalarType.STRING */, repeated: true }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): InputUsageInfo { @@ -574,7 +611,10 @@ export class InputUsageInfo extends Message { return new InputUsageInfo().fromJsonString(jsonString, options); } - static equals(a: InputUsageInfo | PlainMessage | undefined, b: InputUsageInfo | PlainMessage | undefined): boolean { + static equals( + a: InputUsageInfo | PlainMessage | undefined, + b: InputUsageInfo | PlainMessage | undefined, + ): boolean { return proto3.util.equals(InputUsageInfo, a, b); } } @@ -594,9 +634,9 @@ export class PublishGraphQLRequestMetricsRequest extends Message [ - { no: 1, name: "SchemaUsage", kind: "message", T: SchemaUsageInfo, repeated: true }, + { no: 1, name: 'SchemaUsage', kind: 'message', T: SchemaUsageInfo, repeated: true }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): PublishGraphQLRequestMetricsRequest { @@ -611,7 +651,10 @@ export class PublishGraphQLRequestMetricsRequest extends Message | undefined, b: PublishGraphQLRequestMetricsRequest | PlainMessage | undefined): boolean { + static equals( + a: PublishGraphQLRequestMetricsRequest | PlainMessage | undefined, + b: PublishGraphQLRequestMetricsRequest | PlainMessage | undefined, + ): boolean { return proto3.util.equals(PublishGraphQLRequestMetricsRequest, a, b); } } @@ -626,9 +669,8 @@ export class PublishOperationCoverageReportResponse extends Message [ - ]); + static readonly typeName = 'wg.cosmo.graphqlmetrics.v1.PublishOperationCoverageReportResponse'; + static readonly fields: FieldList = proto3.util.newFieldList(() => []); static fromBinary(bytes: Uint8Array, options?: Partial): PublishOperationCoverageReportResponse { return new PublishOperationCoverageReportResponse().fromBinary(bytes, options); @@ -638,11 +680,17 @@ export class PublishOperationCoverageReportResponse extends Message): PublishOperationCoverageReportResponse { + static fromJsonString( + jsonString: string, + options?: Partial, + ): PublishOperationCoverageReportResponse { return new PublishOperationCoverageReportResponse().fromJsonString(jsonString, options); } - static equals(a: PublishOperationCoverageReportResponse | PlainMessage | undefined, b: PublishOperationCoverageReportResponse | PlainMessage | undefined): boolean { + static equals( + a: PublishOperationCoverageReportResponse | PlainMessage | undefined, + b: PublishOperationCoverageReportResponse | PlainMessage | undefined, + ): boolean { return proto3.util.equals(PublishOperationCoverageReportResponse, a, b); } } @@ -662,24 +710,42 @@ export class PublishAggregatedGraphQLRequestMetricsRequest extends Message [ - { no: 1, name: "Aggregation", kind: "message", T: SchemaUsageInfoAggregation, repeated: true }, + { no: 1, name: 'Aggregation', kind: 'message', T: SchemaUsageInfoAggregation, repeated: true }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): PublishAggregatedGraphQLRequestMetricsRequest { + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): PublishAggregatedGraphQLRequestMetricsRequest { return new PublishAggregatedGraphQLRequestMetricsRequest().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): PublishAggregatedGraphQLRequestMetricsRequest { + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): PublishAggregatedGraphQLRequestMetricsRequest { return new PublishAggregatedGraphQLRequestMetricsRequest().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): PublishAggregatedGraphQLRequestMetricsRequest { + static fromJsonString( + jsonString: string, + options?: Partial, + ): PublishAggregatedGraphQLRequestMetricsRequest { return new PublishAggregatedGraphQLRequestMetricsRequest().fromJsonString(jsonString, options); } - static equals(a: PublishAggregatedGraphQLRequestMetricsRequest | PlainMessage | undefined, b: PublishAggregatedGraphQLRequestMetricsRequest | PlainMessage | undefined): boolean { + static equals( + a: + | PublishAggregatedGraphQLRequestMetricsRequest + | PlainMessage + | undefined, + b: + | PublishAggregatedGraphQLRequestMetricsRequest + | PlainMessage + | undefined, + ): boolean { return proto3.util.equals(PublishAggregatedGraphQLRequestMetricsRequest, a, b); } } @@ -694,24 +760,40 @@ export class PublishAggregatedGraphQLRequestMetricsResponse extends Message [ - ]); + static readonly typeName = 'wg.cosmo.graphqlmetrics.v1.PublishAggregatedGraphQLRequestMetricsResponse'; + static readonly fields: FieldList = proto3.util.newFieldList(() => []); - static fromBinary(bytes: Uint8Array, options?: Partial): PublishAggregatedGraphQLRequestMetricsResponse { + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): PublishAggregatedGraphQLRequestMetricsResponse { return new PublishAggregatedGraphQLRequestMetricsResponse().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): PublishAggregatedGraphQLRequestMetricsResponse { + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): PublishAggregatedGraphQLRequestMetricsResponse { return new PublishAggregatedGraphQLRequestMetricsResponse().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): PublishAggregatedGraphQLRequestMetricsResponse { + static fromJsonString( + jsonString: string, + options?: Partial, + ): PublishAggregatedGraphQLRequestMetricsResponse { return new PublishAggregatedGraphQLRequestMetricsResponse().fromJsonString(jsonString, options); } - static equals(a: PublishAggregatedGraphQLRequestMetricsResponse | PlainMessage | undefined, b: PublishAggregatedGraphQLRequestMetricsResponse | PlainMessage | undefined): boolean { + static equals( + a: + | PublishAggregatedGraphQLRequestMetricsResponse + | PlainMessage + | undefined, + b: + | PublishAggregatedGraphQLRequestMetricsResponse + | PlainMessage + | undefined, + ): boolean { return proto3.util.equals(PublishAggregatedGraphQLRequestMetricsResponse, a, b); } } - diff --git a/packages/apollo-to-cosmo-metrics/src/index.ts b/packages/apollo-to-cosmo-metrics/src/index.ts index 499eb0b..0bc6a98 100644 --- a/packages/apollo-to-cosmo-metrics/src/index.ts +++ b/packages/apollo-to-cosmo-metrics/src/index.ts @@ -1,3 +1,3 @@ export { cosmoReportPlugin } from './plugin/exporter.js'; export { CosmoClient } from './plugin/cosmo-client.js'; -export type { Context } from './plugin/exporter.js'; \ No newline at end of file +export type { Context } from './plugin/exporter.js'; diff --git a/packages/apollo-to-cosmo-metrics/src/plugin/cosmo-client.ts b/packages/apollo-to-cosmo-metrics/src/plugin/cosmo-client.ts index 18b6e8f..f1abe88 100644 --- a/packages/apollo-to-cosmo-metrics/src/plugin/cosmo-client.ts +++ b/packages/apollo-to-cosmo-metrics/src/plugin/cosmo-client.ts @@ -1,7 +1,7 @@ -import {Client, createClient} from '@connectrpc/connect'; -import {createConnectTransport} from '@connectrpc/connect-node'; -import {GraphQLMetricsService} from '../generated/graphqlmetrics/v1/graphqlmetrics_connect.js'; -import {type SchemaUsageInfoAggregation} from '../generated/graphqlmetrics/v1/graphqlmetrics_pb.js'; +import { Client, createClient } from '@connectrpc/connect'; +import { createConnectTransport } from '@connectrpc/connect-node'; +import { GraphQLMetricsService } from '../generated/graphqlmetrics/v1/graphqlmetrics_connect.js'; +import { type SchemaUsageInfoAggregation } from '../generated/graphqlmetrics/v1/graphqlmetrics_pb.js'; export type CosmoConfig = { routerToken: string; @@ -34,9 +34,7 @@ export class CosmoClient { // ); try { await this.client.publishAggregatedGraphQLMetrics(aggregatedReports, { - headers: new Headers([ - ['Authorization', `Bearer ${this.config.routerToken}`], - ]), + headers: new Headers([['Authorization', `Bearer ${this.config.routerToken}`]]), }); } catch (e) { console.error('Error sending metrics to Cosmo', e); diff --git a/packages/apollo-to-cosmo-metrics/src/plugin/exporter.ts b/packages/apollo-to-cosmo-metrics/src/plugin/exporter.ts index 2c0d895..9dea713 100644 --- a/packages/apollo-to-cosmo-metrics/src/plugin/exporter.ts +++ b/packages/apollo-to-cosmo-metrics/src/plugin/exporter.ts @@ -28,12 +28,14 @@ import { SchemaUsageInfoAggregation, TypeFieldUsageInfo, } from '../generated/graphqlmetrics/v1/graphqlmetrics_pb.js'; -import {type CosmoClient} from './cosmo-client.js'; +import { type CosmoClient } from './cosmo-client.js'; const CLIENT_NAME_HEADER = 'apollographql-client-name'; const CLIENT_VERSION_HEADER = 'apollographql-client-version'; const INTERVAL_TO_FLASH_IN_MS = 20_000; +const DEFAULT_MAX_QUEUE_SIZE = 10_000; +let isReadingReports = false; let reportQueue: Queue; export interface Context {} @@ -41,16 +43,16 @@ export interface Context {} export function cosmoReportPlugin( client: CosmoClient, reportIntervalMs: number = INTERVAL_TO_FLASH_IN_MS, + maxQueueSize: number = DEFAULT_MAX_QUEUE_SIZE, ): ApolloServerPlugin { const cosmoClient = client; + let interval: NodeJS.Timeout; + return { async serverWillStart() { reportQueue = new Queue(); - const interval = setInterval( - async () => processReports(cosmoClient), - reportIntervalMs, - ); + interval = setInterval(async () => processReports(cosmoClient), reportIntervalMs); return { async serverWillStop() { @@ -61,29 +63,25 @@ export function cosmoReportPlugin( }, async requestDidStart() { return { - async executionDidStart( - context: GraphQLRequestContextExecutionDidStart, - ) { + async executionDidStart(context: GraphQLRequestContextExecutionDidStart) { // Should we report all operations? if (context.operationName === 'IntrospectionQuery') { return; } try { - const metrics = collectMetrics( - context, - context.operation.selectionSet, - ); + const metrics = collectMetrics(context, context.operation.selectionSet); enqueueMetrics(context, metrics); + if (reportQueue.size >= maxQueueSize) { + void processReports(cosmoClient); + clearInterval(interval); + interval = setInterval(async () => processReports(cosmoClient), reportIntervalMs); + } } catch (error: unknown) { const query = context.source.replaceAll(/(\r\n|\n|\r)/gm, ''); - const {variables} = context.request; - const errorMessage = - error instanceof Error ? error.message : String(error); - console.error( - {errorMessage, query, variables}, - 'Cosmo Usage Report Plugin has failed on query', - ); + const { variables } = context.request; + const errorMessage = error instanceof Error ? error.message : String(error); + console.error({ errorMessage, query, variables }, 'Cosmo Usage Report Plugin has failed on query'); } }, }; @@ -92,27 +90,29 @@ export function cosmoReportPlugin( } async function processReports(cosmoClient: CosmoClient) { + if (isReadingReports) { + return; + } + isReadingReports = true; + const reports: SchemaUsageInfoAggregation[] = []; - for (let ii = 0; ii < reportQueue.size; ii++) { + while (reportQueue.size > 0) { const dequeuedItem = reportQueue.dequeue(); if (dequeuedItem) { reports.push(dequeuedItem); - } else { - break; } } if (reports.length === 0) { + isReadingReports = false; return; } + isReadingReports = false; await cosmoClient.reportMetrics(reports); } -function enqueueMetrics( - context: GraphQLRequestContextExecutionDidStart, - metrics: RequestMetrics, -) { +function enqueueMetrics(context: GraphQLRequestContextExecutionDidStart, metrics: RequestMetrics) { const operationInfo = new OperationInfo({ Hash: context.queryHash, Name: context.operationName!, @@ -133,7 +133,7 @@ function enqueueMetrics( OperationInfo: operationInfo, ClientInfo: getClientInfo(context.request), RequestInfo: requestInfo, - SchemaInfo: new SchemaInfo({Version: 'v1'}), + SchemaInfo: new SchemaInfo({ Version: 'v1' }), }), RequestCount: BigInt(1), }); @@ -177,28 +177,21 @@ function collectMetrics( if (!selectionSet) return metrics; for (const selection of selectionSet.selections) { - if ( - selection.kind === Kind.FIELD && - selection.name.value !== '__typename' - ) { + if (selection.kind === Kind.FIELD && selection.name.value !== '__typename') { const updatedPath = [...path, selection.name.value]; let namedType: string; let typeName: string; if (objectType) { - const fieldDef = objectType.astNode!.fields!.find( - (f) => f.name.value === selection.name.value, - )!; + const fieldDef = objectType.astNode!.fields!.find((f) => f.name.value === selection.name.value)!; namedType = inferNamedType(fieldDef.type); typeName = objectType.name; } else { - const {operation} = context.operation; + const { operation } = context.operation; const rootFieldDef = operation === OperationTypeNode.QUERY ? context.schema.getQueryType()?.getFields()[selection.name.value] - : context.schema.getMutationType()?.getFields()[ - selection.name.value - ]; + : context.schema.getMutationType()?.getFields()[selection.name.value]; if (!rootFieldDef) return metrics; namedType = inferNamedType(rootFieldDef.astNode!.type); @@ -215,9 +208,7 @@ function collectMetrics( ); if (selection.arguments?.length) { - metrics.append( - collectArguments(context, updatedPath, selection, typeName), - ); + metrics.append(collectArguments(context, updatedPath, selection, typeName)); } metrics.append( @@ -228,18 +219,10 @@ function collectMetrics( context.schema.getType(namedType) as GraphQLObjectType, ), ); - } else if ( - selection.kind === Kind.INLINE_FRAGMENT && - selection.typeCondition - ) { + } else if (selection.kind === Kind.INLINE_FRAGMENT && selection.typeCondition) { const namedType = selection.typeCondition.name.value; metrics.append( - collectMetrics( - context, - selection.selectionSet, - path, - context.schema.getType(namedType) as GraphQLObjectType, - ), + collectMetrics(context, selection.selectionSet, path, context.schema.getType(namedType) as GraphQLObjectType), ); } } @@ -258,14 +241,10 @@ function collectArguments( const typenameObject = context.schema.getType(typeName) as GraphQLObjectType; const fieldName = path.at(-1); - const fieldDef = typenameObject.astNode!.fields!.find( - (f) => f.name.value === fieldName, - )!; + const fieldDef = typenameObject.astNode!.fields!.find((f) => f.name.value === fieldName)!; for (const argument of fieldNode.arguments) { - const argDef = fieldDef.arguments!.find( - (arg) => arg.name.value === argument.name.value, - ); + const argDef = fieldDef.arguments!.find((arg) => arg.name.value === argument.name.value); if (argDef) { metrics.args.push( @@ -294,11 +273,9 @@ function collectInputs( if (argument.value.kind === Kind.VARIABLE) { // Handle variable input - const variableDef: VariableDefinitionNode = - context.operation.variableDefinitions!.find( - (v) => - v.variable.name.value === (argument.value as VariableNode).name.value, - )!; + const variableDef: VariableDefinitionNode = context.operation.variableDefinitions!.find( + (v) => v.variable.name.value === (argument.value as VariableNode).name.value, + )!; const variableNamedType = inferNamedType(variableDef.type); const varNamedTypeDef = context.schema.getType(variableNamedType)!; // Object input diff --git a/packages/apollo-to-cosmo-metrics/test/index.test.ts b/packages/apollo-to-cosmo-metrics/test/index.test.ts index d9a7ed2..088247f 100644 --- a/packages/apollo-to-cosmo-metrics/test/index.test.ts +++ b/packages/apollo-to-cosmo-metrics/test/index.test.ts @@ -1,8 +1,8 @@ import { describe, expect, afterEach, it, vi, beforeEach } from 'vitest'; -import {isMatch} from 'lodash-es'; -import {ApolloServer, type ApolloServerPlugin} from '@apollo/server'; +import { isMatch } from 'lodash-es'; +import { ApolloServer, type ApolloServerPlugin } from '@apollo/server'; import Queue from '@esm2cjs/yocto-queue'; -import {Context, cosmoReportPlugin, CosmoClient} from '../src/index.js'; +import { Context, cosmoReportPlugin, CosmoClient } from '../src/index.js'; import { ArgumentUsageInfo, InputUsageInfo, @@ -40,17 +40,17 @@ const typeDefs = `#graphql const resolvers = { Query: { async me() { - return {username: 'myusername'}; + return { username: 'myusername' }; }, async authorisedUsers() { - return [{tracks: [{title: 'title1'}, {title: 'title2'}]}]; + return [{ tracks: [{ title: 'title1' }, { title: 'title2' }] }]; }, }, Mutation: { async addUserToSystem(_: any, args: any) { - const {age} = args.input; // eslint-disable-line @typescript-eslint/no-unsafe-assignment - const {username} = args.input; // eslint-disable-line @typescript-eslint/no-unsafe-assignment + const { age } = args.input; // eslint-disable-line @typescript-eslint/no-unsafe-assignment + const { username } = args.input; // eslint-disable-line @typescript-eslint/no-unsafe-assignment return { age: age as number, @@ -66,11 +66,11 @@ describe('Cosmo report plugin: metrics collector (fields, arguments, inputs', () let plugin: ApolloServerPlugin; beforeEach(async () => { - vi.useFakeTimers() + vi.useFakeTimers(); cosmoClient = new CosmoClient({ endpointUrl: 'https://cosmo-metrics.wundergraph.com', routerToken: 'secret', - }) + }); plugin = cosmoReportPlugin(cosmoClient, 2000); testServer = new ApolloServer({ typeDefs, @@ -91,8 +91,7 @@ describe('Cosmo report plugin: metrics collector (fields, arguments, inputs', () query: 'query Me { me { username } }', }); - const metrics: SchemaUsageInfoAggregation | undefined = // eslint-disable-line @typescript-eslint/no-unsafe-assignment - enqueueSpy.mock.calls[0]?.[0]; + const metrics: SchemaUsageInfoAggregation | undefined = enqueueSpy.mock.calls[0]?.[0]; // eslint-disable-line @typescript-eslint/no-unsafe-assignment expect(metrics).toBeDefined(); // Expected field metrics @@ -110,12 +109,8 @@ describe('Cosmo report plugin: metrics collector (fields, arguments, inputs', () Count: BigInt(1), }); - expect( - fieldInArray(me, metrics!.SchemaUsage!.TypeFieldMetrics), - ).toBeTruthy(); - expect( - fieldInArray(username, metrics!.SchemaUsage!.TypeFieldMetrics), - ).toBeTruthy(); + expect(fieldInArray(me, metrics!.SchemaUsage!.TypeFieldMetrics)).toBeTruthy(); + expect(fieldInArray(username, metrics!.SchemaUsage!.TypeFieldMetrics)).toBeTruthy(); // No args or inputs expect(metrics!.SchemaUsage!.ArgumentMetrics.length).toEqual(0); @@ -141,13 +136,11 @@ describe('Cosmo report plugin: metrics collector (fields, arguments, inputs', () const enqueueSpy = vi.spyOn(Queue.prototype, 'enqueue'); const response = await testServer.executeOperation({ - query: - 'mutation AddUser($input: AddUserInput!) { addUserToSystem(input: $input) {... on User { age } } }', - variables: {input: {age: 123, username: 'username123'}}, + query: 'mutation AddUser($input: AddUserInput!) { addUserToSystem(input: $input) {... on User { age } } }', + variables: { input: { age: 123, username: 'username123' } }, }); - const schemaUsageMessage: SchemaUsageInfoAggregation | undefined = // eslint-disable-line @typescript-eslint/no-unsafe-assignment - enqueueSpy.mock.calls[0]?.[0]; + const schemaUsageMessage: SchemaUsageInfoAggregation | undefined = enqueueSpy.mock.calls[0]?.[0]; // eslint-disable-line @typescript-eslint/no-unsafe-assignment expect(schemaUsageMessage).toBeDefined(); expect(schemaUsageMessage!.SchemaUsage?.TypeFieldMetrics.length).toBe(2); @@ -168,15 +161,8 @@ describe('Cosmo report plugin: metrics collector (fields, arguments, inputs', () Count: BigInt(1), }); - expect( - fieldInArray( - addUserToSystem, - schemaUsageMessage!.SchemaUsage!.TypeFieldMetrics, - ), - ).toBeTruthy(); - expect( - fieldInArray(age, schemaUsageMessage!.SchemaUsage!.TypeFieldMetrics), - ).toBeTruthy(); + expect(fieldInArray(addUserToSystem, schemaUsageMessage!.SchemaUsage!.TypeFieldMetrics)).toBeTruthy(); + expect(fieldInArray(age, schemaUsageMessage!.SchemaUsage!.TypeFieldMetrics)).toBeTruthy(); const inputArgument = new ArgumentUsageInfo({ Path: ['addUserToSystem', 'input'], @@ -185,12 +171,7 @@ describe('Cosmo report plugin: metrics collector (fields, arguments, inputs', () Count: BigInt(1), }); - expect( - argInArray( - inputArgument, - schemaUsageMessage!.SchemaUsage!.ArgumentMetrics, - ), - ).toBeTruthy(); + expect(argInArray(inputArgument, schemaUsageMessage!.SchemaUsage!.ArgumentMetrics)).toBeTruthy(); const ageInput = new InputUsageInfo({ Path: ['AddUserInput', 'age'], @@ -204,15 +185,8 @@ describe('Cosmo report plugin: metrics collector (fields, arguments, inputs', () NamedType: 'String', }); - expect( - inputInArray(ageInput, schemaUsageMessage!.SchemaUsage!.InputMetrics), - ).toBeTruthy(); - expect( - inputInArray( - usernameInput, - schemaUsageMessage!.SchemaUsage!.InputMetrics, - ), - ).toBeTruthy(); + expect(inputInArray(ageInput, schemaUsageMessage!.SchemaUsage!.InputMetrics)).toBeTruthy(); + expect(inputInArray(usernameInput, schemaUsageMessage!.SchemaUsage!.InputMetrics)).toBeTruthy(); // Verify that query has not failed expect(response.body.kind).toBe('single'); @@ -235,12 +209,10 @@ describe('Cosmo report plugin: metrics collector (fields, arguments, inputs', () const enqueueSpy = vi.spyOn(Queue.prototype, 'enqueue'); const response = await testServer.executeOperation({ - query: - 'query MyUsers { authorisedUsers { tracks (first: 10 ) { title } } }', + query: 'query MyUsers { authorisedUsers { tracks (first: 10 ) { title } } }', }); - const schemaUsageMessage: SchemaUsageInfoAggregation | undefined = // eslint-disable-line @typescript-eslint/no-unsafe-assignment - enqueueSpy.mock.calls[0]?.[0]; + const schemaUsageMessage: SchemaUsageInfoAggregation | undefined = enqueueSpy.mock.calls[0]?.[0]; // eslint-disable-line @typescript-eslint/no-unsafe-assignment expect(schemaUsageMessage).toBeDefined(); const authorisedUsers = new TypeFieldUsageInfo({ @@ -264,18 +236,9 @@ describe('Cosmo report plugin: metrics collector (fields, arguments, inputs', () Count: BigInt(1), }); - expect( - fieldInArray( - authorisedUsers, - schemaUsageMessage!.SchemaUsage!.TypeFieldMetrics, - ), - ).toBeTruthy(); - expect( - fieldInArray(track, schemaUsageMessage!.SchemaUsage!.TypeFieldMetrics), - ).toBeTruthy(); - expect( - fieldInArray(title, schemaUsageMessage!.SchemaUsage!.TypeFieldMetrics), - ).toBeTruthy(); + expect(fieldInArray(authorisedUsers, schemaUsageMessage!.SchemaUsage!.TypeFieldMetrics)).toBeTruthy(); + expect(fieldInArray(track, schemaUsageMessage!.SchemaUsage!.TypeFieldMetrics)).toBeTruthy(); + expect(fieldInArray(title, schemaUsageMessage!.SchemaUsage!.TypeFieldMetrics)).toBeTruthy(); const firstArgument = new ArgumentUsageInfo({ Path: ['authorisedUsers', 'tracks'], @@ -284,12 +247,7 @@ describe('Cosmo report plugin: metrics collector (fields, arguments, inputs', () Count: BigInt(1), }); - expect( - argInArray( - firstArgument, - schemaUsageMessage!.SchemaUsage!.ArgumentMetrics, - ), - ).toBeTruthy(); + expect(argInArray(firstArgument, schemaUsageMessage!.SchemaUsage!.ArgumentMetrics)).toBeTruthy(); const firstInput = new InputUsageInfo({ Path: ['tracks', 'first'], @@ -297,9 +255,7 @@ describe('Cosmo report plugin: metrics collector (fields, arguments, inputs', () Count: BigInt(1), }); - expect( - inputInArray(firstInput, schemaUsageMessage!.SchemaUsage!.InputMetrics), - ).toBeTruthy(); + expect(inputInArray(firstInput, schemaUsageMessage!.SchemaUsage!.InputMetrics)).toBeTruthy(); // Verify that query has not failed expect(response.body.kind).toBe('single'); @@ -307,7 +263,7 @@ describe('Cosmo report plugin: metrics collector (fields, arguments, inputs', () // @ts-ignore expect(response.body.singleResult.data?.authorisedUsers).toEqual([ { - tracks: [{title: 'title1'}, {title: 'title2'}], + tracks: [{ title: 'title1' }, { title: 'title2' }], }, ]); @@ -319,50 +275,110 @@ describe('Cosmo report plugin: metrics collector (fields, arguments, inputs', () }); it('should sent aggregate message to cosmo with multiple schema usage reports', async () => { - const reportMetricsSpy = vi.spyOn(CosmoClient.prototype, 'reportMetrics').mockResolvedValue(); + const reportMetricsSpy = vi.spyOn(CosmoClient.prototype, 'reportMetrics').mockResolvedValue(); await testServer.executeOperation({ query: 'query Me { me { username } }', }); await testServer.executeOperation({ - query: - 'mutation AddUser($input: AddUserInput!) { addUserToSystem(input: $input) {... on User { age } } }', - variables: {input: {age: 123, username: 'username123'}}, + query: 'mutation AddUser($input: AddUserInput!) { addUserToSystem(input: $input) {... on User { age } } }', + variables: { input: { age: 123, username: 'username123' } }, }); await testServer.executeOperation({ - query: - 'query MyUsers { authorisedUsers { tracks (first: 10 ) { title } } }', + query: 'query MyUsers { authorisedUsers { tracks (first: 10 ) { title } } }', }); vi.advanceTimersByTime(3000); expect(reportMetricsSpy).toHaveBeenCalledTimes(1); expect(reportMetricsSpy).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.anything(), - expect.anything(), - expect.anything(), - ]), + expect.arrayContaining([expect.anything(), expect.anything(), expect.anything()]), ); }); }); -function fieldInArray( - expectedField: Partial, - recieved: TypeFieldUsageInfo[], -): boolean { +function fieldInArray(expectedField: Partial, recieved: TypeFieldUsageInfo[]): boolean { return recieved.some((field) => isMatch(field, expectedField)); } -function argInArray( - expectedArg: Partial, - recieved: ArgumentUsageInfo[], -): boolean { +function argInArray(expectedArg: Partial, recieved: ArgumentUsageInfo[]): boolean { return recieved.some((arg) => isMatch(arg, expectedArg)); } -function inputInArray( - expectedInput: Partial, - recieved: InputUsageInfo[], -): boolean { +function inputInArray(expectedInput: Partial, recieved: InputUsageInfo[]): boolean { return recieved.some((input) => isMatch(input, expectedInput)); } + +describe('Cosmo report plugin: reporting mechanisms', () => { + const maxQueueSize = 100; + let testServer: ApolloServer; + let cosmoClient: CosmoClient; + let plugin: ApolloServerPlugin; + + beforeEach(async () => { + vi.useFakeTimers(); + cosmoClient = new CosmoClient({ + endpointUrl: 'https://cosmo-metrics.wundergraph.com', + routerToken: 'secret', + }); + plugin = cosmoReportPlugin(cosmoClient, 10000, maxQueueSize); + testServer = new ApolloServer({ + typeDefs, + resolvers, + plugins: [plugin], + }); + }); + + afterEach(async () => { + await testServer.stop(); // Stops interval + }); + + it('should execute 100 operations and ensure report is called just once', async () => { + const reportMetricsSpy = vi.spyOn(CosmoClient.prototype, 'reportMetrics').mockResolvedValue(); + const enqueueSpy = vi.spyOn(Queue.prototype, 'enqueue'); + + const promises: Promise[] = []; + for (let i = 0; i < 50; i++) { + promises.push( + testServer.executeOperation({ + query: 'query Me { me { username } }', + }), + ); + } + await Promise.allSettled(promises); + + vi.advanceTimersByTime(10000); + + expect(enqueueSpy).toHaveBeenCalledTimes(50); + expect(reportMetricsSpy).toHaveBeenCalledTimes(1); + + enqueueSpy.mockRestore(); + reportMetricsSpy.mockRestore(); + }); + + it('should send report when the queue is full', async () => { + testServer = new ApolloServer({ + typeDefs, + resolvers, + plugins: [plugin], + }); + + const reportMetricsSpy = vi.spyOn(CosmoClient.prototype, 'reportMetrics').mockResolvedValue(); + const enqueueSpy = vi.spyOn(Queue.prototype, 'enqueue'); + + const promises: Promise[] = []; + for (let i = 0; i < 200; i++) { + promises.push( + testServer.executeOperation({ + query: 'query Me { me { username } }', + }), + ); + } + await Promise.allSettled(promises); + + vi.advanceTimersByTime(3000); + expect(reportMetricsSpy).toHaveBeenCalledTimes(2); + + enqueueSpy.mockRestore(); + reportMetricsSpy.mockRestore(); + }); +}); diff --git a/packages/apollo-to-cosmo-metrics/tsconfig.test.json b/packages/apollo-to-cosmo-metrics/tsconfig.test.json index 317a2ed..14f3a5d 100644 --- a/packages/apollo-to-cosmo-metrics/tsconfig.test.json +++ b/packages/apollo-to-cosmo-metrics/tsconfig.test.json @@ -9,7 +9,7 @@ "sourceMap": true, "outDir": "./dist", "module": "NodeNext", - "noEmit": true, + "noEmit": true }, "include": ["test/**/*"], "exclude": ["node_modules"] diff --git a/packages/cosmo-to-apollo-schema/README.md b/packages/cosmo-to-apollo-schema/README.md index a994b46..1e0f106 100644 --- a/packages/cosmo-to-apollo-schema/README.md +++ b/packages/cosmo-to-apollo-schema/README.md @@ -19,7 +19,7 @@ import { SchemaLoader } from '@wundergraph/cosmo-to-apollo-schema'; // 2. Configure with file, cdn or s3 const cosmoSchemaLoader = new SchemaLoader({ - filePath: "./cosmo-config.json", + filePath: './cosmo-config.json', }); // 3. Pass it to the gateway subgraphSdl @@ -31,7 +31,7 @@ const server = new ApolloServer({ gateway, }); -startStandaloneServer(server) +startStandaloneServer(server); ``` ## Loader Options @@ -61,7 +61,7 @@ interface CDNOptions { } ``` -`endpoint`: The url to the cdn. (default https://cosmo-cdn.wundergraph.com). +`endpoint`: The url to the cdn. (default https://cosmo-cdn.wundergraph.com). `token`: The token for your Federated Graph. You can generate one with the [token create command](https://cosmo-docs.wundergraph.com/cli/router/token/create). @@ -83,7 +83,7 @@ dotenv.config(); // Fetches from Cosmo Cloud CDN by default const cosmoSchemaLoader = new SchemaLoader({ cdn: { - // Token for your federated graph on cosmo. + // Token for your federated graph on cosmo. token: process.env.GRAPH_TOKEN, }, pollInterval: 3000, @@ -107,7 +107,7 @@ startStandaloneServer(server).then(({ url }) => { The plugin watches for any config file changes for the provided path and updates the sdl accordingly. ```ts -filePath: string +filePath: string; ``` ## S3 diff --git a/packages/cosmo-to-apollo-schema/package.json b/packages/cosmo-to-apollo-schema/package.json index baed74d..a7c25d0 100644 --- a/packages/cosmo-to-apollo-schema/package.json +++ b/packages/cosmo-to-apollo-schema/package.json @@ -30,7 +30,8 @@ ], "scripts": { "start": "node ./dist/index.js", - "build": "del dist && tsc" + "build": "del dist && tsc", + "format": "prettier --write -c src" }, "dependencies": { "@apollo/composition": "^2.9.2",