diff --git a/tracker/tracker-graphql/README.md b/tracker/tracker-graphql/README.md index 4cc3a4931..9c68794c5 100644 --- a/tracker/tracker-graphql/README.md +++ b/tracker/tracker-graphql/README.md @@ -18,13 +18,13 @@ returns `result` without changes. ```js import Tracker from '@openreplay/tracker'; -import trackerGraphQL from '@openreplay/tracker-graphql'; +import { createGraphqlMiddleware } from '@openreplay/tracker-graphql'; const tracker = new Tracker({ projectKey: YOUR_PROJECT_KEY, }); -export const recordGraphQL = tracker.plugin(trackerGraphQL()); +export const recordGraphQL = tracker.use(createGraphqlMiddleware()); ``` ### Relay @@ -33,15 +33,28 @@ If you're using [Relay network tools](https://github.com/relay-tools/react-relay you can simply [create a middleware](https://github.com/relay-tools/react-relay-network-modern/tree/master?tab=readme-ov-file#example-of-injecting-networklayer-with-middlewares-on-the-client-side) ```js -import { createRelayMiddleware } from '@openreplay/tracker-graphql' +import { createRelayMiddleware } from '@openreplay/tracker-graphql'; -const trackerMiddleware = createRelayMiddleware(tracker) +const trackerMiddleware = tracker.use(createRelayMiddleware()); const network = new RelayNetworkLayer([ // your middleware // , - trackerMiddleware -]) + trackerMiddleware, +]); +``` + +You can pass a Sanitizer function to `createRelayMiddleware` to sanitize the variables and data before sending them to OpenReplay. + +```js +const trackerLink = tracker.use( + createRelayMiddleware((variables) => { + return { + ...variables, + password: '***', + }; + }), +); ``` Or you can manually put `recordGraphQL` call @@ -52,22 +65,22 @@ then you should do something like below import { createGraphqlMiddleware } from '@openreplay/tracker-graphql'; // see above for recordGraphQL definition import { Environment } from 'relay-runtime'; -const handler = createGraphqlMiddleware(tracker) +const handler = tracker.use(createGraphqlMiddleware()); function fetchQuery(operation, variables, cacheConfig, uploadables) { return fetch('www.myapi.com/resource', { // ... }) - .then(response => response.json()) - .then(result => - handler( - // op kind, name, variables, response, duration (default 0) - operation.operationKind, - operation.name, - variables, - result, - duration, - ), + .then((response) => response.json()) + .then((result) => + handler( + // op kind, name, variables, response, duration (default 0) + operation.operationKind, + operation.name, + variables, + result, + duration, + ), ); } @@ -81,10 +94,23 @@ See [Relay Network Layer](https://relay.dev/docs/en/network-layer) for details. For [Apollo](https://www.apollographql.com/) you should create a new `ApolloLink` ```js -import { createTrackerLink } from '@openreplay/tracker-graphql' +import { createTrackerLink } from '@openreplay/tracker-graphql'; -const trackerLink = createTrackerLink(tracker); -const yourLink = new ApolloLink(trackerLink) +const trackerLink = tracker.use(createTrackerLink()); +const yourLink = new ApolloLink(trackerLink); +``` + +You can pass a Sanitizer function to `createRelayMiddleware` to sanitize the variables and data before sending them to OpenReplay. + +```js +const trackerLink = tracker.use( + createTrackerLink((variables) => { + return { + ...variables, + password: '***', + }; + }), +); ``` Alternatively you can use generic graphql handler: @@ -93,18 +119,21 @@ Alternatively you can use generic graphql handler: import { createGraphqlMiddleware } from '@openreplay/tracker-graphql'; // see above for recordGraphQL definition import { ApolloLink } from 'apollo-link'; -const handler = createGraphqlMiddleware(tracker) +const handler = tracker.use(createGraphqlMiddleware()); const trackerApolloLink = new ApolloLink((operation, forward) => { - return forward(operation).map(result => - handler( + operation.setContext({ start: performance.now() }); + return forward(operation).map((result) => { + const time = performance.now() - operation.getContext().start; + return handler( // op kind, name, variables, response, duration (default 0) operation.query.definitions[0].operation, operation.operationName, operation.variables, result, - ), - ); + time, + ); + }); }); const link = ApolloLink.from([ diff --git a/tracker/tracker-graphql/src/apolloMiddleware.ts b/tracker/tracker-graphql/src/apolloMiddleware.ts index 948186b77..2956b23ef 100644 --- a/tracker/tracker-graphql/src/apolloMiddleware.ts +++ b/tracker/tracker-graphql/src/apolloMiddleware.ts @@ -1,5 +1,6 @@ import { App, Messages } from '@openreplay/tracker'; import Observable from 'zen-observable'; +import { Sanitizer } from './types'; type Operation = { query: Record; @@ -9,48 +10,63 @@ type Operation = { }; type NextLink = (operation: Operation) => Observable>; -export const createTrackerLink = (app: App | null) => { - if (!app) { - return (operation: Operation, forward: NextLink) => forward(operation); - } - return (operation: Operation, forward: NextLink) => { - return new Observable((observer) => { - const start = app.timestamp(); - const observable = forward(operation); - const subscription = observable.subscribe({ - next(value) { - const end = app.timestamp(); - app.send( - Messages.GraphQL( - operation.query.definitions[0].kind, - operation.operationName, - JSON.stringify(operation.variables), - JSON.stringify(value.data), - end - start, - ), - ); - observer.next(value); - }, - error(error) { - const end = app.timestamp(); - app.send( - Messages.GraphQL( - operation.query.definitions[0].kind, - operation.operationName, - JSON.stringify(operation.variables), - JSON.stringify(error), - end - start, - ), - ); - observer.error(error); - }, - complete() { - observer.complete(); - }, - }); +export const createTrackerLink = ( + sanitizer?: Sanitizer | undefined | null>, +) => { + return (app: App | null) => { + if (!app) { + return (operation: Operation, forward: NextLink) => forward(operation); + } + return (operation: Operation, forward: NextLink) => { + return new Observable((observer) => { + const start = app.timestamp(); + const observable = forward(operation); + const subscription = observable.subscribe({ + next(value) { + const end = app.timestamp(); + const operationDefinition = operation.query.definitions[0]; + app.send( + Messages.GraphQL( + operationDefinition.kind === 'OperationDefinition' + ? operationDefinition.operation + : 'unknown?', + operation.operationName, + JSON.stringify( + sanitizer + ? sanitizer(operation.variables) + : operation.variables, + ), + JSON.stringify(sanitizer ? sanitizer(value.data) : value.data), + end - start, + ), + ); + observer.next(value); + }, + error(error) { + const end = app.timestamp(); + app.send( + Messages.GraphQL( + operation.query.definitions[0].kind, + operation.operationName, + JSON.stringify( + sanitizer + ? sanitizer(operation.variables) + : operation.variables, + ), + JSON.stringify(error), + end - start, + ), + ); + observer.error(error); + }, + complete() { + observer.complete(); + }, + }); - return () => subscription.unsubscribe(); - }); + return () => subscription.unsubscribe(); + }); + }; }; }; diff --git a/tracker/tracker-graphql/src/graphqlMiddleware.ts b/tracker/tracker-graphql/src/graphqlMiddleware.ts index e5302d232..79bb75a94 100644 --- a/tracker/tracker-graphql/src/graphqlMiddleware.ts +++ b/tracker/tracker-graphql/src/graphqlMiddleware.ts @@ -1,4 +1,4 @@ -import { App, Messages } from "@openreplay/tracker"; +import { App, Messages } from '@openreplay/tracker'; function createGraphqlMiddleware() { return (app: App | null) => { @@ -10,7 +10,7 @@ function createGraphqlMiddleware() { operationName: string, variables: any, result: any, - duration = 0 + duration = 0, ) => { try { app.send( @@ -30,4 +30,4 @@ function createGraphqlMiddleware() { }; } -export default createGraphqlMiddleware \ No newline at end of file +export default createGraphqlMiddleware; diff --git a/tracker/tracker-graphql/src/index.ts b/tracker/tracker-graphql/src/index.ts index 339836352..5bf81f24c 100644 --- a/tracker/tracker-graphql/src/index.ts +++ b/tracker/tracker-graphql/src/index.ts @@ -1,9 +1,11 @@ import createTrackerLink from './apolloMiddleware.js'; import createRelayMiddleware from './relayMiddleware.js'; import createGraphqlMiddleware from './graphqlMiddleware.js'; +import { Sanitizer } from './types.js'; export { createTrackerLink, createRelayMiddleware, createGraphqlMiddleware, -} \ No newline at end of file + Sanitizer, +}; diff --git a/tracker/tracker-graphql/src/relayMiddleware.ts b/tracker/tracker-graphql/src/relayMiddleware.ts index f1e9cc721..c3aac9d98 100644 --- a/tracker/tracker-graphql/src/relayMiddleware.ts +++ b/tracker/tracker-graphql/src/relayMiddleware.ts @@ -1,37 +1,55 @@ import { App, Messages } from '@openreplay/tracker'; import type { Middleware, RelayRequest } from './relaytypes'; +import { Sanitizer } from './types'; -const createRelayMiddleware = (app: App | null): Middleware => { - if (!app) { - return (next) => async (req) => await next(req); - } - return (next) => async (req) => { - const start = app.timestamp(); - const resp = await next(req) - const end = app.timestamp(); - if ('requests' in req) { - req.requests.forEach((request) => { - app.send(getMessage(request, resp.json as Record, end - start)) - }) - } else { - app.send(getMessage(req, resp.json as Record, end - start)) +const createRelayMiddleware = (sanitizer?: Sanitizer>) => { + return (app: App | null): Middleware => { + if (!app) { + return (next) => async (req) => await next(req); } - return resp; - } + return (next) => async (req) => { + const start = app.timestamp(); + const resp = await next(req); + const end = app.timestamp(); + if ('requests' in req) { + req.requests.forEach((request) => { + app.send( + getMessage( + request, + resp.json as Record, + end - start, + sanitizer, + ), + ); + }); + } else { + app.send( + getMessage( + req, + resp.json as Record, + end - start, + sanitizer, + ), + ); + } + return resp; + }; + }; }; -function getMessage(request: RelayRequest, json: Record, duration: number) { +function getMessage( + request: RelayRequest, + json: Record, + duration: number, + sanitizer?: Sanitizer>, +) { const opKind = request.operation.kind; const opName = request.operation.name; - const vars = JSON.stringify(request.variables) - const opResp = JSON.stringify(json) - return Messages.GraphQL( - opKind, - opName, - vars, - opResp, - duration - ) + const vars = JSON.stringify( + sanitizer ? sanitizer(request.variables) : request.variables, + ); + const opResp = JSON.stringify(sanitizer ? sanitizer(json) : json); + return Messages.GraphQL(opKind, opName, vars, opResp, duration); } -export default createRelayMiddleware +export default createRelayMiddleware; diff --git a/tracker/tracker-graphql/src/relaytypes.ts b/tracker/tracker-graphql/src/relaytypes.ts index 4330918d5..dbd2d200c 100644 --- a/tracker/tracker-graphql/src/relaytypes.ts +++ b/tracker/tracker-graphql/src/relaytypes.ts @@ -1,4 +1,3 @@ - type ConcreteBatch = { kind: 'Batch'; fragment: any; @@ -9,7 +8,7 @@ type ConcreteBatch = { text: string | null; operationKind: string; }; -type Variables = { [name: string]: any }; +export type Variables = { [name: string]: any }; interface FetchOpts { url?: string; method: 'POST' | 'GET'; @@ -17,7 +16,13 @@ interface FetchOpts { body: string | FormData; credentials?: 'same-origin' | 'include' | 'omit'; mode?: 'cors' | 'websocket' | 'navigate' | 'no-cors' | 'same-origin'; - cache?: 'default' | 'no-store' | 'reload' | 'no-cache' | 'force-cache' | 'only-if-cached'; + cache?: + | 'default' + | 'no-store' + | 'reload' + | 'no-cache' + | 'force-cache' + | 'only-if-cached'; redirect?: 'follow' | 'error' | 'manual'; signal?: AbortSignal; [name: string]: any; diff --git a/tracker/tracker-graphql/src/types.ts b/tracker/tracker-graphql/src/types.ts new file mode 100644 index 000000000..441a215e7 --- /dev/null +++ b/tracker/tracker-graphql/src/types.ts @@ -0,0 +1 @@ +export type Sanitizer = (values: T) => Partial;