Skip to content

Commit

Permalink
feat(graphcache): debug logs for cache misses (#3446)
Browse files Browse the repository at this point in the history
  • Loading branch information
JoviDeCroock authored Dec 2, 2023
1 parent e5c67c2 commit 0964a01
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/mean-onions-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@urql/exchange-graphcache': minor
---

Allow the user to debug cache-misses by means of the new `logger` interface on the `cacheExchange`. A field miss will dispatch a `debug` log when it's not marked with `@_optional` or when it's non-nullable in the `schema`.
76 changes: 76 additions & 0 deletions exchanges/graphcache/src/cacheExchange.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,82 @@ describe('data dependencies', () => {
expect(result.mock.calls[1][0].data).toBe(result.mock.calls[0][0].data);
});

it('logs cache misses', () => {
const client = createClient({
url: 'http://0.0.0.0',
exchanges: [],
});
const op = client.createRequestOperation('query', {
key: 1,
query: queryOne,
variables: undefined,
});

const expected = {
__typename: 'Query',
author: {
id: '123',
name: 'Author',
__typename: 'Author',
},
unrelated: {
id: 'unrelated',
__typename: 'Unrelated',
},
};

const response = vi.fn((forwardOp: Operation): OperationResult => {
expect(forwardOp.key).toBe(op.key);
return { ...queryResponse, operation: forwardOp, data: expected };
});

const { source: ops$, next } = makeSubject<Operation>();
const result = vi.fn();
const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share);

const messages: string[] = [];
pipe(
cacheExchange({
logger(severity, message) {
if (severity === 'debug') {
messages.push(message);
}
},
})({ forward, client, dispatchDebug })(ops$),
tap(result),
publish
);

next(op);
next(op);
next({
...op,
query: gql`
query ($id: ID!) {
author(id: $id) {
id
name
}
}
`,
variables: { id: '123' },
});
expect(response).toHaveBeenCalledTimes(1);
expect(result).toHaveBeenCalledTimes(2);

expect(expected).toMatchObject(result.mock.calls[0][0].data);
expect(result.mock.calls[1][0]).toHaveProperty(
'operation.context.meta.cacheOutcome',
'hit'
);
expect(expected).toMatchObject(result.mock.calls[1][0].data);
expect(result.mock.calls[1][0].data).toBe(result.mock.calls[0][0].data);
expect(messages).toEqual([
'No value for field "author" on entity "Query"',
'No value for field "author" with args {"id":"123"} on entity "Query"',
]);
});

it('respects cache-only operations', () => {
const client = createClient({
url: 'http://0.0.0.0',
Expand Down
24 changes: 24 additions & 0 deletions exchanges/graphcache/src/operations/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,18 @@ const readSelection = (
ctx.partial = true;
dataFieldValue = null;
} else if (dataFieldValue === null && directives.required) {
if (
ctx.store.logger &&
process.env.NODE_ENV !== 'production' &&
InMemoryData.currentOperation === 'read'
) {
ctx.store.logger(
'debug',
`Got value "null" for required field "${fieldName}"${
fieldArgs ? ` with args ${JSON.stringify(fieldArgs)}` : ''
} on entity "${entityKey}"`
);
}
dataFieldValue = undefined;
} else {
hasFields = hasFields || fieldName !== '__typename';
Expand All @@ -551,6 +563,18 @@ const readSelection = (
} else if (deferRef) {
hasNext = true;
} else {
if (
ctx.store.logger &&
process.env.NODE_ENV !== 'production' &&
InMemoryData.currentOperation === 'read'
) {
ctx.store.logger(
'debug',
`No value for field "${fieldName}"${
fieldArgs ? ` with args ${JSON.stringify(fieldArgs)}` : ''
} on entity "${entityKey}"`
);
}
// If the field isn't deferred or partial then we have to abort and also reset
// the partial field
ctx.partial = hasPartials;
Expand Down

0 comments on commit 0964a01

Please sign in to comment.