diff --git a/packages/apollo-engine-reporting/src/__tests__/contextualizedStats.test.ts b/packages/apollo-engine-reporting/src/__tests__/contextualizedStats.test.ts new file mode 100644 index 00000000000..e6f9301dc8d --- /dev/null +++ b/packages/apollo-engine-reporting/src/__tests__/contextualizedStats.test.ts @@ -0,0 +1,124 @@ +import { + Trace, +} from 'apollo-engine-reporting-protobuf'; +import { dateToProtoTimestamp } from "../treeBuilder"; +import { ContextualizedStats } from "../contextualizedStats"; +import { DurationHistogram } from "../durationHistogram"; + +describe('Check query latency stats when', () => { + const statsContext = { + clientReferenceId: "reference", + clientVersion: "version" + } + + let baseDate = new Date(); + let nonFederatedTrace = new Trace({ + startTime: dateToProtoTimestamp(baseDate), + endTime: dateToProtoTimestamp(new Date(baseDate.getTime() + 30*1000)), + durationNs: 30*1000, + root: null, + signature: "signature", + details: null, + }); + + it('adding a single trace', () => { + let contextualizedStats = new ContextualizedStats(statsContext); + contextualizedStats.addTrace(nonFederatedTrace); + expect(contextualizedStats.queryLatencyStats.requestCount).toBe(1); + expect(contextualizedStats.queryLatencyStats.latencyCount).toStrictEqual(new DurationHistogram().incrementDuration(30*1000)); + expect(contextualizedStats.queryLatencyStats.requestsWithErrorsCount).toBe(0); + }); + it('adding a fully cached trace', () => { + let contextualizedStats = new ContextualizedStats(statsContext); + contextualizedStats.addTrace(new Trace({ + ...nonFederatedTrace, + fullQueryCacheHit: true + })); + expect(contextualizedStats.queryLatencyStats.requestCount).toBe(1); + expect(contextualizedStats.queryLatencyStats.cacheHits).toBe(1); + expect(contextualizedStats.queryLatencyStats.cacheLatencyCount).toStrictEqual(new DurationHistogram().incrementDuration(30*1000)); + }); + it('adding a public cached trace', () => { + let contextualizedStats = new ContextualizedStats(statsContext); + contextualizedStats.addTrace(new Trace({ + ...nonFederatedTrace, + fullQueryCacheHit: false, + cachePolicy: { + scope: Trace.CachePolicy.Scope.PRIVATE, + maxAgeNs: 1000 + } + })); + expect(contextualizedStats.queryLatencyStats.requestCount).toBe(1); + expect(contextualizedStats.queryLatencyStats.privateCacheTtlCount).toStrictEqual(new DurationHistogram().incrementDuration(1000)); + }); + it('adding a private cached trace', () => { + let contextualizedStats = new ContextualizedStats(statsContext); + contextualizedStats.addTrace(new Trace({ + ...nonFederatedTrace, + fullQueryCacheHit: false, + cachePolicy: { + scope: Trace.CachePolicy.Scope.PUBLIC, + maxAgeNs: 1000 + } + })); + expect(contextualizedStats.queryLatencyStats.requestCount).toBe(1); + expect(contextualizedStats.queryLatencyStats.publicCacheTtlCount).toStrictEqual(new DurationHistogram().incrementDuration(1000)); + }); + it('adding a persisted hit trace', () => { + let contextualizedStats = new ContextualizedStats(statsContext); + contextualizedStats.addTrace(new Trace({ + ...nonFederatedTrace, + persistedQueryHit: true + })); + expect(contextualizedStats.queryLatencyStats.requestCount).toBe(1); + expect(contextualizedStats.queryLatencyStats.persistedQueryHits).toBe(1); + }); + it('adding a persisted miss trace', () => { + let contextualizedStats = new ContextualizedStats(statsContext); + contextualizedStats.addTrace(new Trace({ + ...nonFederatedTrace, + persistedQueryRegister: true + })); + expect(contextualizedStats.queryLatencyStats.requestCount).toBe(1); + expect(contextualizedStats.queryLatencyStats.persistedQueryMisses).toBe(1); + }); + it('adding a forbidden trace', () => { + let contextualizedStats = new ContextualizedStats(statsContext); + contextualizedStats.addTrace(new Trace({ + ...nonFederatedTrace, + forbiddenOperation: true, + })); + expect(contextualizedStats.queryLatencyStats.requestCount).toBe(1); + expect(contextualizedStats.queryLatencyStats.forbiddenOperationCount).toBe(1); + }); + it('adding a registered trace', () => { + let contextualizedStats = new ContextualizedStats(statsContext); + contextualizedStats.addTrace(new Trace({ + ...nonFederatedTrace, + registeredOperation: true, + })); + expect(contextualizedStats.queryLatencyStats.requestCount).toBe(1); + expect(contextualizedStats.queryLatencyStats.registeredOperationCount).toBe(1); + }); + it('adding an errored trace ', () => { + let contextualizedStats = new ContextualizedStats(statsContext); + contextualizedStats.addTrace(new Trace({ + ...nonFederatedTrace, + registeredOperation: true, + root: { + child: [{ + responseName: "user", + parentType: "Query", + type: "User!", + error: [{ + message: "error 1" + }] + }] + } + })); + expect(contextualizedStats.queryLatencyStats.requestCount).toBe(1); + console.log(contextualizedStats.queryLatencyStats.rootErrorStats); + expect(contextualizedStats.queryLatencyStats.rootErrorStats.children["user"].requestsWithErrorsCount).toBe(1); + expect(contextualizedStats.queryLatencyStats.rootErrorStats.children["user"].errorsCount).toBe(1); + }); +}) diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index ce2c53e9723..03cd5841320 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -32,7 +32,7 @@ import { reportingLoop, SchemaReporter } from './schemaReporter'; import { v4 as uuidv4 } from 'uuid'; import { createHash } from 'crypto'; import { DurationHistogram } from './durationHistogram'; -import { ContextualizedStats } from './ContextualizedStats'; +import { ContextualizedStats } from './contextualizedStats'; let warnedOnDeprecatedApiKey = false; @@ -743,6 +743,8 @@ export class EngineReportingAgent { const statsKeys = Object.keys(report.tracesPerQuery); for (const statsKey of statsKeys) { + // Take the contextualized stats from the map and push them onto the + // statsContext array const statsMap: Map = (report .tracesPerQuery[statsKey] as any).statsMap; if (statsMap) { diff --git a/packages/apollo-engine-reporting/src/ContextualizedStats.ts b/packages/apollo-engine-reporting/src/contextualizedStats.ts similarity index 94% rename from packages/apollo-engine-reporting/src/ContextualizedStats.ts rename to packages/apollo-engine-reporting/src/contextualizedStats.ts index 90f67ede79b..2182c73806c 100644 --- a/packages/apollo-engine-reporting/src/ContextualizedStats.ts +++ b/packages/apollo-engine-reporting/src/contextualizedStats.ts @@ -45,14 +45,14 @@ export class ContextualizedStats { ); } - if (!trace.fullQueryCacheHit && trace.cachePolicy && trace.cachePolicy) { + if (!trace.fullQueryCacheHit && trace.cachePolicy && trace.cachePolicy.maxAgeNs) { if (trace.cachePolicy.scope == Trace.CachePolicy.Scope.PRIVATE) { ((queryLatencyStats.privateCacheTtlCount as unknown) as DurationHistogram).incrementDuration( - trace.cachePolicy.maxAgeNs || 0, + trace.cachePolicy.maxAgeNs ); } else if (trace.cachePolicy.scope == Trace.CachePolicy.Scope.PUBLIC) { ((queryLatencyStats.publicCacheTtlCount as unknown) as DurationHistogram).incrementDuration( - trace.cachePolicy.maxAgeNs || 0, + trace.cachePolicy.maxAgeNs ); } } @@ -69,13 +69,12 @@ export class ContextualizedStats { queryLatencyStats.registeredOperationCount++; } - queryLatencyStats.requestsWithErrorsCount++; - let hasError = false; const typeStats = this.perTypeStat; const rootPathErrorStats = queryLatencyStats.rootErrorStats as IPathErrorStats; function traceNodeStats(node: Trace.INode, path: ReadonlyArray) { + // Generate error stats and error path information if (node.error && node.error.length > 0) { hasError = true; @@ -93,14 +92,16 @@ export class ContextualizedStats { [k: string]: IPathErrorStats; })[subPath]; if (!nextPathErrorStats) { - nextPathErrorStats = Object.create(null)( - children as { [k: string]: IPathErrorStats }, + nextPathErrorStats = Object.create(null); + ( + children as { [k: string]: IPathErrorStats } )[subPath] = nextPathErrorStats; } - // nextPathErrorStats be null or undefined + // nextPathErrorStats cannot be null or undefined currPathErrorStats = nextPathErrorStats as IPathErrorStats; } + currPathErrorStats.requestsWithErrorsCount = (currPathErrorStats.requestsWithErrorsCount || 0) + 1; currPathErrorStats.errorsCount = @@ -223,7 +224,7 @@ function iterateOverTraceNode( childPath = path.concat(child.responseName); } - iterateOverTraceNode(node, childPath, f); + iterateOverTraceNode(child, childPath, f); } } f(node, path); diff --git a/packages/apollo-engine-reporting/src/durationHistogram.ts b/packages/apollo-engine-reporting/src/durationHistogram.ts index 9b924ce25d6..84bf9975b49 100644 --- a/packages/apollo-engine-reporting/src/durationHistogram.ts +++ b/packages/apollo-engine-reporting/src/durationHistogram.ts @@ -39,8 +39,9 @@ export class DurationHistogram { : unboundedBucket; } - public incrementDuration(durationNs: number) { + public incrementDuration(durationNs: number): DurationHistogram { this.incrementBucket(DurationHistogram.durationToBucket(durationNs)); + return this; } public incrementBucket(bucket: number, value = 1) { diff --git a/packages/apollo-engine-reporting/src/treeBuilder.ts b/packages/apollo-engine-reporting/src/treeBuilder.ts index 00e33bd0595..f37c5f71066 100644 --- a/packages/apollo-engine-reporting/src/treeBuilder.ts +++ b/packages/apollo-engine-reporting/src/treeBuilder.ts @@ -259,7 +259,7 @@ function errorToProtobufError(error: GraphQLError): Trace.Error { } // Converts a JS Date into a Timestamp. -function dateToProtoTimestamp(date: Date): google.protobuf.Timestamp { +export function dateToProtoTimestamp(date: Date): google.protobuf.Timestamp { const totalMillis = +date; const millis = totalMillis % 1000; return new google.protobuf.Timestamp({