Import graphql-extensions+apollo-engine-reporting/no global apollo-server-env (#1259)
* Export polyfills and types separately
* More imports from apollo-server-env
* Initial commit
* Add .npmignore to avoid ignoring lib when publishing
* 0.0.2
* Reorganize code and clean up GraphQLExtension interface
* 0.0.3
* Add support for timing callbacks and add GraphQLExtensionStack
* 0.0.4
* Downgrade target in tsconfig.json from es2015 to es5
* 0.0.5
* Bump `graphql` peerDependency. (#3)
* 0.0.6
* Update dependencies
* 0.0.7
* whenResultIsFinished fix for array results (#4)
* 0.0.8
* [apollo-bot] Update the Issue/PR Templates with auto label (#6)
* Bump `graphql` peerDependency. (#7)
* Update `graphql` peer dependency range to allow 0.13.x. (#8)
* Update `devDependencies` to latest versions. (#9)
* dev: Update TypeScript to latest version, v2.7.2.
* dev: Update `graphql` to latest version, v0.13.2.
* dev: Update jest & dependencies to latest versions.
* dev: Update type definitions for `graphql`, `node` and `jest`.
* Allow `undefined` return values to `GraphQLExtension`'s `format()`. (#10)
In some cases, it's conceivable that the `format()` method may need to abort
its decision to provide extension information at runtime, in the event that
it doesn't have the proper information to return a full-result.
The `format` method already removed false-y results, so this simply changes
the types to allow the same.
* 0.0.9
* Fix lifecycle method invocations on extensions
* 0.0.10
* Add changelog
* Upgrade to TypeScript 2.8
Makes my editor integration happier (a bugfix in tsserver I think)
* Add tslint and prettier
Same configuration as apollo-engine-js
* Remove magic from GraphQLExtensionStack constructor
It's not hard to consistently pass in an actual extension object to this
low-level API.
* New extension API: didStart handlers return didEnd handlers
This is a backwards-incompatible change: GraphQLExtension implementations and
users of GraphQLExtensionStack (ie apollo-server-core) must change their
implementations, if they implement any of the xDidStart/xDidEnd APIs.
This allows "didEnd" handlers to refer to closure variables from the "didStart"
handler rather than needing to store state on the extension.
The new "didEnd" handlers run in the opposite order of the "didStart" handlers,
so that they properly nest.
* 0.1.0-beta.0
* Changelog
* Add magic back into GraphQLExtensionStack constructor
But now it actually gets more context (the execution arguments) and doesn't have
to be a constructor.
* 0.1.0-beta.1
* Export more types
* 0.1.0-beta.2
* Fix lifecycle handlers to pass proper "this"
* 0.1.0-beta.3
* Pass options directly to start handlers; eliminate factory again
* 0.1.0-beta.4
* error handling in didEnd
* 0.1.0-beta.5
* pass multiple errors to EndHandler
* 0.1.0-beta.6
* add willSendResponse
* 0.1.0-beta.7
* prettier
* setFieldResolver for custom fieldResolver
* reverse
* get more initial options into requestDidStart
* 0.1.0-beta.8
* 0.1.0-beta.9
* Actually, we already get the fieldResolver!
* 0.1.0-beta.10
* work without extensionStack
* 0.1.0-beta.11
* 0.1.0-beta.12
* Send errors to willResolveField callback
* 0.1.0-beta.13
* willSendResponse can return a result
* 0.1.0-beta.14
* Revert 1063be8..56912fc
This reverts commit 1063be8..56912fc.
* add PQ options to requestDidStart
* 0.1.0-beta.14
* 0.1.0-beta.15
* Initialize an empty TypeScript/Jest package
Template based on apollo-engine-js
* Basic trace node structure building
* basic timing
* Checkpoint towards signature implementation
The new signature implementation does not try to compress whitespace.
* Basic signature implementation
* progress towards actual reporting
* basic checkpoint for reporting
* 0.0.0-beta.1
* pull in @types/long, since it is in the external api
* 0.0.0-beta.2
* get rid of Long
* 0.0.0-beta.3
* debug log request what happened
* 0.0.0-beta.4
* 0.0.0-beta.5
* correct url
* 0.0.0-beta.6
* request headers
* 0.0.0-beta.7
* leave out a few headers
* 0.0.0-beta.8
* prettier
* move stuff into multiple files, and stop exporting the extension
* lots of doc comments
* address agent.ts XXX comments
* implement privateVariables
simplify API by removing flush() and allowing flush-ers to just call sendReport
directly
* privateHeaders and error tracking
* gzip, signals
* fix test
* 0.0.0-beta.9
* Error handling for reports
* 0.0.0-beta.10
* no need to include boring stacktrace
* 0.0.0-beta.11
* tweak error reporting
* 0.0.0-beta.12
* package-lock update (npm@6?)
* Reduce target report size to 512KB from 4MB.
Load testing revealed that protobuf encoding for large FullTraceReports could
tie up CPU and reduce p99 request latency (eg, to 200ms from 10ms). Reducing the
default target report size spreads out the encoding time and mitigates the
impact on latency. If this is not acceptable for all users, we may have to
investigate reintroducing agent-side stats aggregation to keep report sizes
small.
* 0.0.0-beta.13
* Encode Traces as they come in
This improves p99 times with little effect on p50 times. It also lets us get rid
of the heuristic average trace size estimation.
* 0.0.0-beta.14
* support PQ fields
* npm audit fix
* 0.0.0-beta.15
* ignore coverage
* Make the default signature more aggressive
We'd rather tell people confused by literal removal to tweak the signature than
tell people causing outages to do so.
* 0.0.0-beta.16
* Remove obsolete files from graphql-extensions and apollo-engine-reporting
* Fix dependencies and configs
* Fix apollo-server-cloudflare to import from apollo-server-env
* Fix compilation and test configs
* Get all tests passing again
* Switch to Lerna independent versioning
* Polyfill promisify for Node < 8 and load polyfills in tests
* ES2016 exponentiation operator is only supported in Node > 6
* add dependency cache for circle
* add missing env dependencies in REST datasource
2018-06-28 01:29:00 +02:00
|
|
|
import { Request, URL } from 'apollo-server-env';
|
|
|
|
|
|
|
|
import {
|
|
|
|
GraphQLResolveInfo,
|
|
|
|
responsePathAsArray,
|
|
|
|
ResponsePath,
|
|
|
|
DocumentNode,
|
|
|
|
ExecutionArgs,
|
|
|
|
GraphQLError,
|
|
|
|
} from 'graphql';
|
|
|
|
import {
|
|
|
|
GraphQLExtension,
|
|
|
|
GraphQLResponse,
|
|
|
|
EndHandler,
|
|
|
|
} from 'graphql-extensions';
|
|
|
|
import { Trace, google } from 'apollo-engine-reporting-protobuf';
|
|
|
|
|
|
|
|
import { EngineReportingOptions } from './agent';
|
|
|
|
import { defaultSignature } from './signature';
|
|
|
|
|
|
|
|
// EngineReportingExtension is the per-request GraphQLExtension which creates a
|
|
|
|
// trace (in protobuf Trace format) for a single request. When the request is
|
|
|
|
// done, it passes the Trace back to its associated EngineReportingAgent via the
|
|
|
|
// addTrace callback in its constructor. This class isn't for direct use; its
|
|
|
|
// constructor is a private API for communicating with EngineReportingAgent.
|
|
|
|
// Its public methods all implement the GraphQLExtension interface.
|
|
|
|
export class EngineReportingExtension<TContext = any>
|
|
|
|
implements GraphQLExtension<TContext> {
|
|
|
|
public trace = new Trace();
|
|
|
|
private nodes = new Map<string, Trace.Node>();
|
|
|
|
private startHrTime!: [number, number];
|
|
|
|
private operationName?: string;
|
|
|
|
private queryString?: string;
|
|
|
|
private documentAST?: DocumentNode;
|
|
|
|
private options: EngineReportingOptions;
|
|
|
|
private addTrace: (
|
|
|
|
signature: string,
|
|
|
|
operationName: string,
|
|
|
|
trace: Trace,
|
|
|
|
) => void;
|
|
|
|
|
|
|
|
public constructor(
|
|
|
|
options: EngineReportingOptions,
|
|
|
|
addTrace: (signature: string, operationName: string, trace: Trace) => void,
|
|
|
|
) {
|
|
|
|
this.options = options;
|
|
|
|
this.addTrace = addTrace;
|
|
|
|
const root = new Trace.Node();
|
|
|
|
this.trace.root = root;
|
|
|
|
this.nodes.set(responsePathAsString(undefined), root);
|
|
|
|
}
|
|
|
|
|
|
|
|
public requestDidStart(o: {
|
|
|
|
request: Request;
|
|
|
|
queryString?: string;
|
|
|
|
parsedQuery?: DocumentNode;
|
|
|
|
variables: Record<string, any>;
|
|
|
|
persistedQueryHit?: boolean;
|
|
|
|
persistedQueryRegister?: boolean;
|
|
|
|
}): EndHandler {
|
|
|
|
this.trace.startTime = dateToTimestamp(new Date());
|
|
|
|
this.startHrTime = process.hrtime();
|
|
|
|
|
|
|
|
// Generally, we'll get queryString here and not parsedQuery; we only get
|
|
|
|
// parsedQuery if you're using an OperationStore. In normal cases we'll get
|
|
|
|
// our documentAST in the execution callback after it is parsed.
|
|
|
|
this.queryString = o.queryString;
|
|
|
|
this.documentAST = o.parsedQuery;
|
|
|
|
|
2018-06-28 18:29:26 +02:00
|
|
|
let host: string | null;
|
|
|
|
let path: string;
|
|
|
|
// On Node's HTTP module, message.url only includes the path
|
|
|
|
// (see https://nodejs.org/api/http.html#http_message_url)
|
|
|
|
// The same is true on Lambda (where we pass event.path)
|
|
|
|
// That isn't a URL and parsing will fail, so we just set the path directly.
|
|
|
|
if (o.request.url.startsWith('/')) {
|
|
|
|
host = null;
|
|
|
|
path = o.request.url;
|
|
|
|
} else {
|
|
|
|
const url = new URL(o.request.url);
|
|
|
|
host = url.hostname;
|
|
|
|
path = url.pathname;
|
|
|
|
}
|
Import graphql-extensions+apollo-engine-reporting/no global apollo-server-env (#1259)
* Export polyfills and types separately
* More imports from apollo-server-env
* Initial commit
* Add .npmignore to avoid ignoring lib when publishing
* 0.0.2
* Reorganize code and clean up GraphQLExtension interface
* 0.0.3
* Add support for timing callbacks and add GraphQLExtensionStack
* 0.0.4
* Downgrade target in tsconfig.json from es2015 to es5
* 0.0.5
* Bump `graphql` peerDependency. (#3)
* 0.0.6
* Update dependencies
* 0.0.7
* whenResultIsFinished fix for array results (#4)
* 0.0.8
* [apollo-bot] Update the Issue/PR Templates with auto label (#6)
* Bump `graphql` peerDependency. (#7)
* Update `graphql` peer dependency range to allow 0.13.x. (#8)
* Update `devDependencies` to latest versions. (#9)
* dev: Update TypeScript to latest version, v2.7.2.
* dev: Update `graphql` to latest version, v0.13.2.
* dev: Update jest & dependencies to latest versions.
* dev: Update type definitions for `graphql`, `node` and `jest`.
* Allow `undefined` return values to `GraphQLExtension`'s `format()`. (#10)
In some cases, it's conceivable that the `format()` method may need to abort
its decision to provide extension information at runtime, in the event that
it doesn't have the proper information to return a full-result.
The `format` method already removed false-y results, so this simply changes
the types to allow the same.
* 0.0.9
* Fix lifecycle method invocations on extensions
* 0.0.10
* Add changelog
* Upgrade to TypeScript 2.8
Makes my editor integration happier (a bugfix in tsserver I think)
* Add tslint and prettier
Same configuration as apollo-engine-js
* Remove magic from GraphQLExtensionStack constructor
It's not hard to consistently pass in an actual extension object to this
low-level API.
* New extension API: didStart handlers return didEnd handlers
This is a backwards-incompatible change: GraphQLExtension implementations and
users of GraphQLExtensionStack (ie apollo-server-core) must change their
implementations, if they implement any of the xDidStart/xDidEnd APIs.
This allows "didEnd" handlers to refer to closure variables from the "didStart"
handler rather than needing to store state on the extension.
The new "didEnd" handlers run in the opposite order of the "didStart" handlers,
so that they properly nest.
* 0.1.0-beta.0
* Changelog
* Add magic back into GraphQLExtensionStack constructor
But now it actually gets more context (the execution arguments) and doesn't have
to be a constructor.
* 0.1.0-beta.1
* Export more types
* 0.1.0-beta.2
* Fix lifecycle handlers to pass proper "this"
* 0.1.0-beta.3
* Pass options directly to start handlers; eliminate factory again
* 0.1.0-beta.4
* error handling in didEnd
* 0.1.0-beta.5
* pass multiple errors to EndHandler
* 0.1.0-beta.6
* add willSendResponse
* 0.1.0-beta.7
* prettier
* setFieldResolver for custom fieldResolver
* reverse
* get more initial options into requestDidStart
* 0.1.0-beta.8
* 0.1.0-beta.9
* Actually, we already get the fieldResolver!
* 0.1.0-beta.10
* work without extensionStack
* 0.1.0-beta.11
* 0.1.0-beta.12
* Send errors to willResolveField callback
* 0.1.0-beta.13
* willSendResponse can return a result
* 0.1.0-beta.14
* Revert 1063be8..56912fc
This reverts commit 1063be8..56912fc.
* add PQ options to requestDidStart
* 0.1.0-beta.14
* 0.1.0-beta.15
* Initialize an empty TypeScript/Jest package
Template based on apollo-engine-js
* Basic trace node structure building
* basic timing
* Checkpoint towards signature implementation
The new signature implementation does not try to compress whitespace.
* Basic signature implementation
* progress towards actual reporting
* basic checkpoint for reporting
* 0.0.0-beta.1
* pull in @types/long, since it is in the external api
* 0.0.0-beta.2
* get rid of Long
* 0.0.0-beta.3
* debug log request what happened
* 0.0.0-beta.4
* 0.0.0-beta.5
* correct url
* 0.0.0-beta.6
* request headers
* 0.0.0-beta.7
* leave out a few headers
* 0.0.0-beta.8
* prettier
* move stuff into multiple files, and stop exporting the extension
* lots of doc comments
* address agent.ts XXX comments
* implement privateVariables
simplify API by removing flush() and allowing flush-ers to just call sendReport
directly
* privateHeaders and error tracking
* gzip, signals
* fix test
* 0.0.0-beta.9
* Error handling for reports
* 0.0.0-beta.10
* no need to include boring stacktrace
* 0.0.0-beta.11
* tweak error reporting
* 0.0.0-beta.12
* package-lock update (npm@6?)
* Reduce target report size to 512KB from 4MB.
Load testing revealed that protobuf encoding for large FullTraceReports could
tie up CPU and reduce p99 request latency (eg, to 200ms from 10ms). Reducing the
default target report size spreads out the encoding time and mitigates the
impact on latency. If this is not acceptable for all users, we may have to
investigate reintroducing agent-side stats aggregation to keep report sizes
small.
* 0.0.0-beta.13
* Encode Traces as they come in
This improves p99 times with little effect on p50 times. It also lets us get rid
of the heuristic average trace size estimation.
* 0.0.0-beta.14
* support PQ fields
* npm audit fix
* 0.0.0-beta.15
* ignore coverage
* Make the default signature more aggressive
We'd rather tell people confused by literal removal to tweak the signature than
tell people causing outages to do so.
* 0.0.0-beta.16
* Remove obsolete files from graphql-extensions and apollo-engine-reporting
* Fix dependencies and configs
* Fix apollo-server-cloudflare to import from apollo-server-env
* Fix compilation and test configs
* Get all tests passing again
* Switch to Lerna independent versioning
* Polyfill promisify for Node < 8 and load polyfills in tests
* ES2016 exponentiation operator is only supported in Node > 6
* add dependency cache for circle
* add missing env dependencies in REST datasource
2018-06-28 01:29:00 +02:00
|
|
|
|
|
|
|
this.trace.http = new Trace.HTTP({
|
|
|
|
method:
|
|
|
|
Trace.HTTP.Method[o.request.method as keyof typeof Trace.HTTP.Method] ||
|
|
|
|
Trace.HTTP.Method.UNKNOWN,
|
2018-06-28 18:29:26 +02:00
|
|
|
host,
|
|
|
|
path,
|
Import graphql-extensions+apollo-engine-reporting/no global apollo-server-env (#1259)
* Export polyfills and types separately
* More imports from apollo-server-env
* Initial commit
* Add .npmignore to avoid ignoring lib when publishing
* 0.0.2
* Reorganize code and clean up GraphQLExtension interface
* 0.0.3
* Add support for timing callbacks and add GraphQLExtensionStack
* 0.0.4
* Downgrade target in tsconfig.json from es2015 to es5
* 0.0.5
* Bump `graphql` peerDependency. (#3)
* 0.0.6
* Update dependencies
* 0.0.7
* whenResultIsFinished fix for array results (#4)
* 0.0.8
* [apollo-bot] Update the Issue/PR Templates with auto label (#6)
* Bump `graphql` peerDependency. (#7)
* Update `graphql` peer dependency range to allow 0.13.x. (#8)
* Update `devDependencies` to latest versions. (#9)
* dev: Update TypeScript to latest version, v2.7.2.
* dev: Update `graphql` to latest version, v0.13.2.
* dev: Update jest & dependencies to latest versions.
* dev: Update type definitions for `graphql`, `node` and `jest`.
* Allow `undefined` return values to `GraphQLExtension`'s `format()`. (#10)
In some cases, it's conceivable that the `format()` method may need to abort
its decision to provide extension information at runtime, in the event that
it doesn't have the proper information to return a full-result.
The `format` method already removed false-y results, so this simply changes
the types to allow the same.
* 0.0.9
* Fix lifecycle method invocations on extensions
* 0.0.10
* Add changelog
* Upgrade to TypeScript 2.8
Makes my editor integration happier (a bugfix in tsserver I think)
* Add tslint and prettier
Same configuration as apollo-engine-js
* Remove magic from GraphQLExtensionStack constructor
It's not hard to consistently pass in an actual extension object to this
low-level API.
* New extension API: didStart handlers return didEnd handlers
This is a backwards-incompatible change: GraphQLExtension implementations and
users of GraphQLExtensionStack (ie apollo-server-core) must change their
implementations, if they implement any of the xDidStart/xDidEnd APIs.
This allows "didEnd" handlers to refer to closure variables from the "didStart"
handler rather than needing to store state on the extension.
The new "didEnd" handlers run in the opposite order of the "didStart" handlers,
so that they properly nest.
* 0.1.0-beta.0
* Changelog
* Add magic back into GraphQLExtensionStack constructor
But now it actually gets more context (the execution arguments) and doesn't have
to be a constructor.
* 0.1.0-beta.1
* Export more types
* 0.1.0-beta.2
* Fix lifecycle handlers to pass proper "this"
* 0.1.0-beta.3
* Pass options directly to start handlers; eliminate factory again
* 0.1.0-beta.4
* error handling in didEnd
* 0.1.0-beta.5
* pass multiple errors to EndHandler
* 0.1.0-beta.6
* add willSendResponse
* 0.1.0-beta.7
* prettier
* setFieldResolver for custom fieldResolver
* reverse
* get more initial options into requestDidStart
* 0.1.0-beta.8
* 0.1.0-beta.9
* Actually, we already get the fieldResolver!
* 0.1.0-beta.10
* work without extensionStack
* 0.1.0-beta.11
* 0.1.0-beta.12
* Send errors to willResolveField callback
* 0.1.0-beta.13
* willSendResponse can return a result
* 0.1.0-beta.14
* Revert 1063be8..56912fc
This reverts commit 1063be8..56912fc.
* add PQ options to requestDidStart
* 0.1.0-beta.14
* 0.1.0-beta.15
* Initialize an empty TypeScript/Jest package
Template based on apollo-engine-js
* Basic trace node structure building
* basic timing
* Checkpoint towards signature implementation
The new signature implementation does not try to compress whitespace.
* Basic signature implementation
* progress towards actual reporting
* basic checkpoint for reporting
* 0.0.0-beta.1
* pull in @types/long, since it is in the external api
* 0.0.0-beta.2
* get rid of Long
* 0.0.0-beta.3
* debug log request what happened
* 0.0.0-beta.4
* 0.0.0-beta.5
* correct url
* 0.0.0-beta.6
* request headers
* 0.0.0-beta.7
* leave out a few headers
* 0.0.0-beta.8
* prettier
* move stuff into multiple files, and stop exporting the extension
* lots of doc comments
* address agent.ts XXX comments
* implement privateVariables
simplify API by removing flush() and allowing flush-ers to just call sendReport
directly
* privateHeaders and error tracking
* gzip, signals
* fix test
* 0.0.0-beta.9
* Error handling for reports
* 0.0.0-beta.10
* no need to include boring stacktrace
* 0.0.0-beta.11
* tweak error reporting
* 0.0.0-beta.12
* package-lock update (npm@6?)
* Reduce target report size to 512KB from 4MB.
Load testing revealed that protobuf encoding for large FullTraceReports could
tie up CPU and reduce p99 request latency (eg, to 200ms from 10ms). Reducing the
default target report size spreads out the encoding time and mitigates the
impact on latency. If this is not acceptable for all users, we may have to
investigate reintroducing agent-side stats aggregation to keep report sizes
small.
* 0.0.0-beta.13
* Encode Traces as they come in
This improves p99 times with little effect on p50 times. It also lets us get rid
of the heuristic average trace size estimation.
* 0.0.0-beta.14
* support PQ fields
* npm audit fix
* 0.0.0-beta.15
* ignore coverage
* Make the default signature more aggressive
We'd rather tell people confused by literal removal to tweak the signature than
tell people causing outages to do so.
* 0.0.0-beta.16
* Remove obsolete files from graphql-extensions and apollo-engine-reporting
* Fix dependencies and configs
* Fix apollo-server-cloudflare to import from apollo-server-env
* Fix compilation and test configs
* Get all tests passing again
* Switch to Lerna independent versioning
* Polyfill promisify for Node < 8 and load polyfills in tests
* ES2016 exponentiation operator is only supported in Node > 6
* add dependency cache for circle
* add missing env dependencies in REST datasource
2018-06-28 01:29:00 +02:00
|
|
|
});
|
|
|
|
if (this.options.privateHeaders !== true) {
|
|
|
|
for (const [key, value] of o.request.headers) {
|
|
|
|
if (
|
|
|
|
this.options.privateHeaders &&
|
|
|
|
typeof this.options.privateHeaders === 'object' &&
|
|
|
|
// We assume that most users only have a few private headers, or will
|
|
|
|
// just set privateHeaders to true; we can change this linear-time
|
|
|
|
// operation if it causes real performance issues.
|
|
|
|
this.options.privateHeaders.includes(key.toLowerCase())
|
|
|
|
) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (key) {
|
|
|
|
case 'authorization':
|
|
|
|
case 'cookie':
|
|
|
|
case 'set-cookie':
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
this.trace.http!.requestHeaders![key] = new Trace.HTTP.Values({
|
|
|
|
value: [value],
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (o.persistedQueryHit) {
|
|
|
|
this.trace.persistedQueryHit = true;
|
|
|
|
}
|
|
|
|
if (o.persistedQueryRegister) {
|
|
|
|
this.trace.persistedQueryRegister = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.options.privateVariables !== true && o.variables) {
|
|
|
|
// Note: we explicitly do *not* include the details.rawQuery field. The
|
|
|
|
// Engine web app currently does nothing with this other than store it in
|
|
|
|
// the database and offer it up via its GraphQL API, and sending it means
|
|
|
|
// that using calculateSignature to hide sensitive data in the query
|
|
|
|
// string is ineffective.
|
|
|
|
this.trace.details = new Trace.Details();
|
|
|
|
Object.keys(o.variables).forEach(name => {
|
|
|
|
if (
|
|
|
|
this.options.privateVariables &&
|
|
|
|
typeof this.options.privateVariables === 'object' &&
|
|
|
|
// We assume that most users will have only a few private variables,
|
|
|
|
// or will just set privateVariables to true; we can change this
|
|
|
|
// linear-time operation if it causes real performance issues.
|
|
|
|
this.options.privateVariables.includes(name)
|
|
|
|
) {
|
|
|
|
// Special case for private variables. Note that this is a different
|
|
|
|
// representation from a variable containing the empty string, as that
|
|
|
|
// will be sent as '""'.
|
|
|
|
this.trace.details!.variablesJson![name] = '';
|
|
|
|
} else {
|
|
|
|
this.trace.details!.variablesJson![name] = JSON.stringify(
|
|
|
|
o.variables[name],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
this.trace.durationNs = durationHrTimeToNanos(
|
|
|
|
process.hrtime(this.startHrTime),
|
|
|
|
);
|
|
|
|
this.trace.endTime = dateToTimestamp(new Date());
|
|
|
|
|
|
|
|
const operationName = this.operationName || '';
|
|
|
|
let signature;
|
|
|
|
if (this.documentAST) {
|
|
|
|
const calculateSignature =
|
|
|
|
this.options.calculateSignature || defaultSignature;
|
|
|
|
signature = calculateSignature(this.documentAST, operationName);
|
|
|
|
} else if (this.queryString) {
|
|
|
|
// We didn't get an AST, possibly because of a parse failure. Let's just
|
|
|
|
// use the full query string.
|
|
|
|
//
|
|
|
|
// XXX This does mean that even if you use a calculateSignature which
|
|
|
|
// hides literals, you might end up sending literals for queries
|
|
|
|
// that fail parsing or validation. Provide some way to mask them
|
|
|
|
// anyway?
|
|
|
|
signature = this.queryString;
|
|
|
|
} else {
|
|
|
|
// This shouldn't happen: one of those options must be passed to runQuery.
|
|
|
|
throw new Error('No queryString or parsedQuery?');
|
|
|
|
}
|
|
|
|
|
|
|
|
this.addTrace(signature, operationName, this.trace);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
public executionDidStart(o: { executionArgs: ExecutionArgs }) {
|
|
|
|
// If the operationName is explicitly provided, save it. If there's just one
|
|
|
|
// named operation, the client doesn't have to provide it, but we still want
|
|
|
|
// to know the operation name so that the server can identify the query by
|
|
|
|
// it without having to parse a signature.
|
|
|
|
//
|
|
|
|
// Fortunately, in the non-error case, we can just pull this out of
|
|
|
|
// the first call to willResolveField's `info` argument. In an
|
|
|
|
// error case (eg, the operationName isn't found, or there are more
|
|
|
|
// than one operation and no specified operationName) it's OK to continue
|
|
|
|
// to file this trace under the empty operationName.
|
|
|
|
if (o.executionArgs.operationName) {
|
|
|
|
this.operationName = o.executionArgs.operationName;
|
|
|
|
}
|
|
|
|
this.documentAST = o.executionArgs.document;
|
|
|
|
}
|
|
|
|
|
|
|
|
public willResolveField(
|
|
|
|
_source: any,
|
|
|
|
_args: { [argName: string]: any },
|
|
|
|
_context: TContext,
|
|
|
|
info: GraphQLResolveInfo,
|
|
|
|
): ((error: Error | null, result: any) => void) | void {
|
|
|
|
if (this.operationName === undefined) {
|
|
|
|
this.operationName =
|
|
|
|
(info.operation.name && info.operation.name.value) || '';
|
|
|
|
}
|
|
|
|
|
|
|
|
const path = info.path;
|
|
|
|
const node = this.newNode(path);
|
|
|
|
node.type = info.returnType.toString();
|
|
|
|
node.parentType = info.parentType.toString();
|
|
|
|
node.startTime = durationHrTimeToNanos(process.hrtime(this.startHrTime));
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
node.endTime = durationHrTimeToNanos(process.hrtime(this.startHrTime));
|
|
|
|
// We could save the error into the trace here, but it won't have all
|
|
|
|
// the information that graphql-js adds to it later, like 'locations'.
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
public willSendResponse(o: { graphqlResponse: GraphQLResponse }) {
|
|
|
|
const { errors } = o.graphqlResponse;
|
|
|
|
if (errors) {
|
|
|
|
errors.forEach((error: GraphQLError) => {
|
|
|
|
// By default, put errors on the root node.
|
|
|
|
let node = this.nodes.get('');
|
|
|
|
if (error.path) {
|
|
|
|
const specificNode = this.nodes.get(error.path.join('.'));
|
|
|
|
if (specificNode) {
|
|
|
|
node = specificNode;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
node!.error!.push(
|
|
|
|
new Trace.Error({
|
|
|
|
message: error.message,
|
|
|
|
location: (error.locations || []).map(
|
|
|
|
({ line, column }) => new Trace.Location({ line, column }),
|
|
|
|
),
|
|
|
|
json: JSON.stringify(error),
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private newNode(path: ResponsePath): Trace.Node {
|
|
|
|
const node = new Trace.Node();
|
|
|
|
const id = path.key;
|
|
|
|
if (typeof id === 'number') {
|
|
|
|
node.index = id;
|
|
|
|
} else {
|
|
|
|
node.fieldName = id;
|
|
|
|
}
|
|
|
|
this.nodes.set(responsePathAsString(path), node);
|
|
|
|
const parentNode = this.ensureParentNode(path);
|
|
|
|
parentNode.child.push(node);
|
|
|
|
return node;
|
|
|
|
}
|
|
|
|
|
|
|
|
private ensureParentNode(path: ResponsePath): Trace.Node {
|
|
|
|
const parentPath = responsePathAsString(path.prev);
|
|
|
|
const parentNode = this.nodes.get(parentPath);
|
|
|
|
if (parentNode) {
|
|
|
|
return parentNode;
|
|
|
|
}
|
|
|
|
// Because we set up the root path in the constructor, we now know that
|
|
|
|
// path.prev isn't undefined.
|
|
|
|
return this.newNode(path.prev!);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Helpers for producing traces.
|
|
|
|
|
|
|
|
// Convert from the linked-list ResponsePath format to a dot-joined
|
|
|
|
// string. Includes the full path (field names and array indices).
|
|
|
|
function responsePathAsString(p: ResponsePath | undefined) {
|
|
|
|
if (p === undefined) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
return responsePathAsArray(p).join('.');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Converts a JS Date into a Timestamp.
|
|
|
|
function dateToTimestamp(date: Date): google.protobuf.Timestamp {
|
|
|
|
const totalMillis = +date;
|
|
|
|
const millis = totalMillis % 1000;
|
|
|
|
return new google.protobuf.Timestamp({
|
|
|
|
seconds: (totalMillis - millis) / 1000,
|
|
|
|
nanos: millis * 1e6,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Converts an hrtime array (as returned from process.hrtime) to nanoseconds.
|
|
|
|
//
|
|
|
|
// ONLY CALL THIS ON VALUES REPRESENTING DELTAS, NOT ON THE RAW RETURN VALUE
|
|
|
|
// FROM process.hrtime() WITH NO ARGUMENTS.
|
|
|
|
//
|
|
|
|
// The entire point of the hrtime data structure is that the JavaScript Number
|
|
|
|
// type can't represent all int64 values without loss of precision:
|
|
|
|
// Number.MAX_SAFE_INTEGER nanoseconds is about 104 days. Calling this function
|
|
|
|
// on a duration that represents a value less than 104 days is fine. Calling
|
|
|
|
// this function on an absolute time (which is generally roughly time since
|
|
|
|
// system boot) is not a good idea.
|
|
|
|
//
|
|
|
|
// XXX We should probably use google.protobuf.Duration on the wire instead of
|
|
|
|
// ever trying to store durations in a single number.
|
|
|
|
function durationHrTimeToNanos(hrtime: [number, number]) {
|
|
|
|
return hrtime[0] * 1e9 + hrtime[1];
|
|
|
|
}
|