Skip to content

Commit

Permalink
refact: abstract custom plugins (#10509)
Browse files Browse the repository at this point in the history
  • Loading branch information
kewitz authored Dec 11, 2024
1 parent 95ecaf3 commit 9bff203
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 53 deletions.
3 changes: 2 additions & 1 deletion config/custom-environment-variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
},
"error": {
"detailed": "GRAPHQL_ERROR_DETAILED"
}
},
"resolverTimeDebugWarning": "GRAPHQL_RESOLVER_TIME_DEBUG"
},
"memcache": {
"servers": "MEMCACHE_SERVERS",
Expand Down
3 changes: 2 additions & 1 deletion config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,8 @@
},
"error": {
"detailed": false
}
},
"resolverTimeDebugWarning": 200
},
"pdfService": {
"fetchTransactionsReceipts": false
Expand Down
71 changes: 71 additions & 0 deletions server/lib/apollo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import config from 'config';
import { orderBy, round, uniqBy } from 'lodash';

import cache from './cache';
import logger from './logger';
import { sentryHandleSlowRequests } from './sentry';

const minExecutionTimeToCache = parseInt(config.graphql.cache.minExecutionTimeToCache);

const enablePluginIf = (condition, plugin) => (condition ? plugin : {});

export const apolloSlowRequestCachePlugin = {
async requestDidStart() {
return {
async willSendResponse(requestContext) {
const response = requestContext.response;
const result = response?.body?.singleResult;
if (!result) {
return;
}

const req = requestContext.contextValue; // From apolloExpressMiddlewareOptions context()

req.endAt = req.endAt || new Date();
const executionTime = req.endAt - req.startAt;
req.res.set('Execution-Time', executionTime);

// Track all slow queries on Sentry performance
sentryHandleSlowRequests(executionTime);

// This will never happen for logged-in users as cacheKey is not set
if (req.cacheKey && !response?.errors && executionTime > minExecutionTimeToCache) {
cache.set(req.cacheKey, result, Number(config.graphql.cache.ttl));
}
},
};
},
};

const resolverTimeDebugWarning = parseInt(config.graphql?.resolverTimeDebugWarning || '200');

export const apolloSlowResolverDebugPlugin = enablePluginIf(config.env === 'development', {
async requestDidStart() {
return {
async executionDidStart(executionRequestContext) {
const slow = [];
return {
willResolveField({ info }) {
const start = process.hrtime.bigint();
return () => {
const end = process.hrtime.bigint();
slow.push({
timeMs: round(Number(end - start) / 1e6, 2),
field: `${info.parentType.name}.${info.fieldName}`,
});
};
},
executionDidEnd() {
uniqBy(orderBy(slow, ['timeNs'], ['desc']), 'field')
.filter(s => s.timeMs >= resolverTimeDebugWarning)
.forEach(s => {
logger.warn(
`${executionRequestContext.operation.name.value} slow fields: ${s.field} took ${Number(s.timeMs)}ms`,
);
});
},
};
},
};
},
});
77 changes: 26 additions & 51 deletions server/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { paypalWebhook, plaidWebhook, stripeWebhook, transferwiseWebhook } from
import { getGraphqlCacheProperties } from './graphql/cache';
import graphqlSchemaV1 from './graphql/v1/schema';
import graphqlSchemaV2 from './graphql/v2/schema';
import { apolloSlowRequestCachePlugin, apolloSlowResolverDebugPlugin } from './lib/apollo';
import cache from './lib/cache';
import errors from './lib/errors';
import expressLimiter from './lib/express-limiter';
Expand Down Expand Up @@ -129,29 +130,30 @@ export default async app => {
/**
* GraphQL caching
*/
app.use('/graphql', async (req, res, next) => {
req.startAt = req.startAt || new Date();
const { cacheKey, cacheSlug } = getGraphqlCacheProperties(req) || {}; // Returns null if not cacheable (e.g. if logged in)
const enabled = parseToBoolean(config.graphql.cache.enabled);
if (cacheKey && enabled) {
const fromCache = await cache.get(cacheKey);
if (fromCache) {
// Track all slow queries on Sentry performance
res.servedFromGraphqlCache = true;
req.endAt = req.endAt || new Date();
const executionTime = req.endAt - req.startAt;
sentryHandleSlowRequests(executionTime);
res.set('Execution-Time', executionTime);
res.set('GraphQL-Cache', 'HIT');
res.send(fromCache);
return;
if (parseToBoolean(config.graphql.cache.enabled)) {
app.use('/graphql', async (req, res, next) => {
req.startAt = req.startAt || new Date();
const { cacheKey, cacheSlug } = getGraphqlCacheProperties(req) || {}; // Returns null if not cacheable (e.g. if logged in)
if (cacheKey) {
const fromCache = await cache.get(cacheKey);
if (fromCache) {
// Track all slow queries on Sentry performance
res.servedFromGraphqlCache = true;
req.endAt = req.endAt || new Date();
const executionTime = req.endAt - req.startAt;
sentryHandleSlowRequests(executionTime);
res.set('Execution-Time', executionTime);
res.set('GraphQL-Cache', 'HIT');
res.send(fromCache);
return;
}
res.set('GraphQL-Cache', 'MISS');
req.cacheKey = cacheKey;
req.cacheSlug = cacheSlug;
}
res.set('GraphQL-Cache', 'MISS');
req.cacheKey = cacheKey;
req.cacheSlug = cacheSlug;
}
next();
});
next();
});
}

/**
* GraphQL scope
Expand Down Expand Up @@ -229,8 +231,6 @@ export default async app => {
graphqlPlugins.push(SentryGraphQLPlugin);
}

const minExecutionTimeToCache = parseInt(config.graphql.cache.minExecutionTimeToCache);

const httpServer = http.createServer(app);

const apolloServerOptions = {
Expand Down Expand Up @@ -264,33 +264,8 @@ export default async app => {
...graphqlProtection,
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
{
async requestDidStart() {
return {
async willSendResponse(requestContext) {
const response = requestContext.response;
const result = response?.body?.singleResult;
if (!result) {
return;
}

const req = requestContext.contextValue; // From apolloExpressMiddlewareOptions context()

req.endAt = req.endAt || new Date();
const executionTime = req.endAt - req.startAt;
req.res.set('Execution-Time', executionTime);

// Track all slow queries on Sentry performance
sentryHandleSlowRequests(executionTime);

// This will never happen for logged-in users as cacheKey is not set
if (req.cacheKey && !response?.errors && executionTime > minExecutionTimeToCache) {
cache.set(req.cacheKey, result, Number(config.graphql.cache.ttl));
}
},
};
},
},
apolloSlowRequestCachePlugin,
apolloSlowResolverDebugPlugin,
...graphqlPlugins,
],
};
Expand Down

0 comments on commit 9bff203

Please sign in to comment.