Skip to content

Commit 6f850f2

Browse files
committed
feat(js-api-client): Add methods to profile request.
1 parent 3a979b6 commit 6f850f2

File tree

4 files changed

+131
-21
lines changed

4 files changed

+131
-21
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ So far, the available helpers are:
2121
- CustomerManager
2222
- Subscription Contract Manager
2323
- Signature Verification
24+
- Profiling the requests
2425

2526
## Installation
2627

@@ -507,6 +508,33 @@ The guard function will trigger an exception.
507508

508509
> We let you provide the `sha256` and `jwtVerify` methods to stay agnostic of any library.
509510
511+
## Profiling the request
512+
513+
There is time when you want to log and see the raw queries sent to Crystallize and also the timings.
514+
515+
```javascript
516+
const apiClient = createClient(
517+
{
518+
tenantIdentifier: 'furniture',
519+
},
520+
{
521+
profiling: {
522+
onRequest: (query, variables) => {
523+
// do something with it
524+
console.log(query, variables);
525+
},
526+
onRequestResolved: (processingTimeMs, query, variables) => {
527+
// do something with it
528+
console.log(processingTimeMs, query, variables);
529+
},
530+
},
531+
},
532+
);
533+
```
534+
535+
The queries that you get passed on those functions are strings. Computed query from JS Object used by Fetcher, Hydrater and so on.
536+
It's really handy for debugging and development.
537+
510538
## Mass Call Client
511539

512540
Sometimes, when you have many calls to do, whether they are queries or mutations, you want to be able to manage them asynchronously. This is the purpose of the Mass Call Client. It will let you be asynchronous, managing the heavy lifting of lifecycle, retry, incremental increase or decrease of the pace, etc.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@crystallize/js-api-client",
33
"license": "MIT",
4-
"version": "1.10.0",
4+
"version": "1.11.0",
55
"author": "Crystallize <[email protected]> (https://crystallize.com)",
66
"contributors": [
77
"Sébastien Morel <[email protected]>"

src/core/client.ts

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ export type ClientConfiguration = {
1010
origin?: string;
1111
};
1212

13+
type ProfilingOptions = {
14+
onRequest: (query: string, variables?: VariablesType) => void;
15+
onRequestResolved: (processingTimeMs: number, query: string, variables?: VariablesType) => void;
16+
};
17+
18+
export type CreateClientOptions = {
19+
profiling?: ProfilingOptions;
20+
};
21+
1322
export type VariablesType = Record<string, any>;
1423
export type ApiCaller<T> = (query: string, variables?: VariablesType) => Promise<T>;
1524

@@ -45,6 +54,7 @@ async function post<T>(
4554
query: string,
4655
variables?: VariablesType,
4756
init?: RequestInit | any | undefined,
57+
profiling?: ProfilingOptions,
4858
): Promise<T> {
4959
try {
5060
const commonHeaders = {
@@ -56,6 +66,11 @@ async function post<T>(
5666
...authenticationHeaders(config),
5767
};
5868
const body = JSON.stringify({ query, variables });
69+
let start: number = 0;
70+
if (profiling) {
71+
start = Date.now();
72+
profiling.onRequest(query, variables);
73+
}
5974

6075
const response = await fetch(path, {
6176
...init,
@@ -64,6 +79,10 @@ async function post<T>(
6479
body,
6580
});
6681

82+
if (profiling) {
83+
const ms = Date.now() - start;
84+
profiling.onRequestResolved(ms, query, variables);
85+
}
6786
if (response.ok && 204 === response.status) {
6887
return <T>{};
6988
}
@@ -93,34 +112,26 @@ async function post<T>(
93112
}
94113
}
95114

96-
function createApiCaller(uri: string, configuration: ClientConfiguration): ApiCaller<any> {
97-
/**
98-
* Call a crystallize. Will automatically handle access tokens
99-
* @param query The GraphQL query
100-
* @param variables Variables to inject into query.
101-
*/
115+
function createApiCaller(
116+
uri: string,
117+
configuration: ClientConfiguration,
118+
options?: CreateClientOptions,
119+
): ApiCaller<any> {
102120
return function callApi<T>(query: string, variables?: VariablesType): Promise<T> {
103-
return post<T>(uri, configuration, query, variables);
121+
return post<T>(uri, configuration, query, variables, undefined, options?.profiling);
104122
};
105123
}
106124

107-
/**
108-
* Create one api client for each api endpoint Crystallize offers (catalogue, search, order, subscription, pim).
109-
*
110-
* @param configuration
111-
* @returns ClientInterface
112-
*/
113-
export function createClient(configuration: ClientConfiguration): ClientInterface {
125+
export function createClient(configuration: ClientConfiguration, options?: CreateClientOptions): ClientInterface {
114126
const identifier = configuration.tenantIdentifier;
115127
const origin = configuration.origin || '.crystallize.com';
116128
const apiHost = (path: string[], prefix: 'api' | 'pim' = 'api') => `https://${prefix}${origin}/${path.join('/')}`;
117-
118129
return {
119-
catalogueApi: createApiCaller(apiHost([identifier, 'catalogue']), configuration),
120-
searchApi: createApiCaller(apiHost([identifier, 'search']), configuration),
121-
orderApi: createApiCaller(apiHost([identifier, 'orders']), configuration),
122-
subscriptionApi: createApiCaller(apiHost([identifier, 'subscriptions']), configuration),
123-
pimApi: createApiCaller(apiHost(['graphql'], 'pim'), configuration),
130+
catalogueApi: createApiCaller(apiHost([identifier, 'catalogue']), configuration, options),
131+
searchApi: createApiCaller(apiHost([identifier, 'search']), configuration, options),
132+
orderApi: createApiCaller(apiHost([identifier, 'orders']), configuration, options),
133+
subscriptionApi: createApiCaller(apiHost([identifier, 'subscriptions']), configuration, options),
134+
pimApi: createApiCaller(apiHost(['graphql'], 'pim'), configuration, options),
124135
config: {
125136
tenantId: configuration.tenantId,
126137
tenantIdentifier: configuration.tenantIdentifier,

tests/catalogue-profiled.test.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
const { createClient, createCatalogueFetcher } = require('../dist/index.js');
2+
const { VariableType } = require('json-to-graphql-query');
3+
4+
test('Test with Profiling 1 ', async () => {
5+
const variables = {
6+
language: 'en',
7+
path: '/shop/chairs/bamboo-chair',
8+
};
9+
10+
const expectedQuery = `query ($language: String!, $path: String!) { catalogue (language: $language, path: $path) { ... on Product { name } } }`;
11+
12+
let profiledQuery = {};
13+
14+
const apiClient = createClient(
15+
{
16+
tenantIdentifier: 'furniture',
17+
},
18+
{
19+
profiling: {
20+
onRequest: (query, variables) => {
21+
profiledQuery = {
22+
...profiledQuery,
23+
onRequest: {
24+
query,
25+
variables,
26+
},
27+
};
28+
},
29+
onRequestResolved: (processingTimeMs, query, variables) => {
30+
profiledQuery = {
31+
...profiledQuery,
32+
onRequestResolved: {
33+
processingTimeMs,
34+
query,
35+
variables,
36+
},
37+
};
38+
},
39+
},
40+
},
41+
);
42+
43+
const fetcher = createCatalogueFetcher(apiClient);
44+
const response = await fetcher(
45+
{
46+
__variables: {
47+
language: 'String!',
48+
path: 'String!',
49+
},
50+
catalogue: {
51+
__args: {
52+
language: new VariableType('language'),
53+
path: new VariableType('path'),
54+
},
55+
__on: {
56+
__typeName: 'Product',
57+
name: true,
58+
},
59+
},
60+
},
61+
variables,
62+
);
63+
64+
expect(response.catalogue.name).toBe('Bamboo Chair');
65+
expect(profiledQuery.onRequest.query).toBe(expectedQuery);
66+
expect(profiledQuery.onRequest.variables).toBe(variables);
67+
68+
expect(profiledQuery.onRequestResolved.query).toBe(expectedQuery);
69+
expect(profiledQuery.onRequestResolved.variables).toBe(variables);
70+
expect(profiledQuery.onRequestResolved.processingTimeMs).toBeGreaterThan(0);
71+
});

0 commit comments

Comments
 (0)