Tracker GrahpQL: update doc and tracker initialization + add option to pass sanitizer function (#2402)

* fix(graphQL): update doc and tracker initialization + add option to pass sanitizer function

* improvement(graphQL): improve sanitizer type & apollo operation name
This commit is contained in:
PiR 2024-07-22 16:07:12 +02:00 committed by GitHub
parent ddd037ce79
commit 754293e29d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 171 additions and 100 deletions

View file

@ -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([

View file

@ -1,5 +1,6 @@
import { App, Messages } from '@openreplay/tracker';
import Observable from 'zen-observable';
import { Sanitizer } from './types';
type Operation = {
query: Record<string, any>;
@ -9,48 +10,63 @@ type Operation = {
};
type NextLink = (operation: Operation) => Observable<Record<string, any>>;
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<Record<string, any> | 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();
});
};
};
};

View file

@ -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
export default createGraphqlMiddleware;

View file

@ -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,
}
Sanitizer,
};

View file

@ -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<string, any>, end - start))
})
} else {
app.send(getMessage(req, resp.json as Record<string, any>, end - start))
const createRelayMiddleware = (sanitizer?: Sanitizer<Record<string, any>>) => {
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<string, any>,
end - start,
sanitizer,
),
);
});
} else {
app.send(
getMessage(
req,
resp.json as Record<string, any>,
end - start,
sanitizer,
),
);
}
return resp;
};
};
};
function getMessage(request: RelayRequest, json: Record<string, any>, duration: number) {
function getMessage(
request: RelayRequest,
json: Record<string, any>,
duration: number,
sanitizer?: Sanitizer<Record<string, any>>,
) {
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;

View file

@ -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;

View file

@ -0,0 +1 @@
export type Sanitizer<T> = (values: T) => Partial<T>;