Skip to content

Commit

Permalink
Merge pull request #1915 from NDLANO/graphql-error-logging
Browse files Browse the repository at this point in the history
Add requestPath and other graphql context to graphql errors
  • Loading branch information
jnatten authored Jul 12, 2024
2 parents bcc95f1 + 11efc3a commit 3fe773c
Show file tree
Hide file tree
Showing 10 changed files with 90 additions and 44 deletions.
2 changes: 1 addition & 1 deletion src/iframe/embedIframeIndex.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const language = initialProps.locale ?? config.defaultLocale;

const cache = createCache({ key: EmotionCacheKey });

const client = createApolloClient(language);
const client = createApolloClient(language, undefined, window.location.pathname);
const i18n = initializeI18n(i18nInstance, language);

const renderOrHydrate = (container: HTMLElement, children: ReactNode) => {
Expand Down
9 changes: 5 additions & 4 deletions src/server/podcastRssFeed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*
*/

import { Request } from "express";
import { gql, ApolloClient, NormalizedCacheObject } from "@apollo/client/core";
import config from "../config";
import { GQLPodcastSeriesQuery } from "../graphqlTypes";
Expand All @@ -14,18 +15,18 @@ import { createApolloClient } from "../util/apiHelpers";
let apolloClient: ApolloClient<NormalizedCacheObject>;
let storedLocale: string;

const getApolloClient = (locale: string) => {
const getApolloClient = (locale: string, req: Request) => {
if (apolloClient && locale === storedLocale) {
return apolloClient;
} else {
apolloClient = createApolloClient(locale);
apolloClient = createApolloClient(locale, undefined, req.path);
storedLocale = locale;
return apolloClient;
}
};

const podcastRssFeed = async (seriesId: number): Promise<string> => {
const client = getApolloClient("nb");
const podcastRssFeed = async (seriesId: number, req: Request): Promise<string> => {
const client = getApolloClient("nb", req);

const { data: { podcastSeries } = {} } = await client.query<GQLPodcastSeriesQuery>({
query: podcastSeriesQuery,
Expand Down
2 changes: 1 addition & 1 deletion src/server/render/defaultRender.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export const defaultRender: RenderFunc = async (req) => {
};
}

const client = createApolloClient(locale, versionHash);
const client = createApolloClient(locale, versionHash, req.path);
const cache = createCache({ key: EmotionCacheKey });
const i18n = initializeI18n(i18nInstance, locale);
const redirectContext: RedirectInfo = {};
Expand Down
2 changes: 1 addition & 1 deletion src/server/render/iframeArticleRender.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const iframeArticleRender: RenderFunc = async (req) => {
};
}

const client = createApolloClient(locale);
const client = createApolloClient(locale, undefined, req.path);
const cache = createCache({ key: EmotionCacheKey });
const i18n = initializeI18n(i18nInstance, locale ?? config.defaultLocale);
const context: RedirectInfo = {};
Expand Down
2 changes: 1 addition & 1 deletion src/server/render/iframeEmbedRender.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const iframeEmbedRender: RenderFunc = async (req) => {
};
}

const client = createApolloClient(locale);
const client = createApolloClient(locale, undefined, req.path);
const cache = createCache({ key: EmotionCacheKey });
const i18n = initializeI18n(i18nInstance, locale ?? config.defaultLocale);
const context: RedirectInfo = {};
Expand Down
6 changes: 3 additions & 3 deletions src/server/routes/oembedArticleRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ type MatchParams = "resourceId" | "topicId" | "lang" | "articleId";
let apolloClient: ApolloClient<NormalizedCacheObject>;
let storedLocale: string;

const getApolloClient = (locale: string) => {
const getApolloClient = (locale: string, req: express.Request) => {
if (apolloClient && locale === storedLocale) {
return apolloClient;
} else {
apolloClient = createApolloClient(locale);
apolloClient = createApolloClient(locale, undefined, req.path);
storedLocale = locale;
return apolloClient;
}
Expand Down Expand Up @@ -111,7 +111,7 @@ const embedOembedQuery = gql`
`;

const getEmbedObject = async (lang: string, embedId: string, embedType: string, req: express.Request) => {
const client = getApolloClient(lang);
const client = getApolloClient(lang, req);

const embed = await client.query<GQLEmbedOembedQuery, GQLEmbedOembedQueryVariables>({
query: embedOembedQuery,
Expand Down
2 changes: 1 addition & 1 deletion src/server/routes/podcastFeedRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const podcastFeedRoute = async (req: Request, res: Response, next: NextFu
return;
}

await podcastRssFeed(id)
await podcastRssFeed(id, req)
.then((podcastPage) => {
res.setHeader("Content-Type", "application/xml");
res.send(podcastPage);
Expand Down
2 changes: 1 addition & 1 deletion src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ async function sendInternalServerError(req: Request, res: Response, err?: Error)

const errorHandler = (err: Error, req: Request, res: Response, __: (err: Error) => void) => {
vite?.ssrFixStacktrace(err);
handleError(err, undefined, req);
handleError(err, undefined, req.path);
sendInternalServerError(req, res, err);
};

Expand Down
61 changes: 37 additions & 24 deletions src/util/apiHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
*
*/

import { GraphQLErrorExtensions } from "graphql/error";
import { ApolloClient, ApolloLink, FieldFunctionOptions, InMemoryCache, TypePolicies } from "@apollo/client/core";
import { BatchHttpLink } from "@apollo/client/link/batch-http";
import { setContext } from "@apollo/client/link/context";
import { onError } from "@apollo/client/link/error";
import { getAccessToken, getFeideCookie, isAccessTokenValid, renewAuth } from "./authHelpers";
import handleError from "./handleError";
import handleError, { LogLevel } from "./handleError";
import config from "../config";
import { GQLBucketResult, GQLGroupSearch, GQLQueryFolderResourceMetaSearchArgs } from "../graphqlTypes";

Expand Down Expand Up @@ -195,16 +196,23 @@ function getCache() {
return cache;
}

export const createApolloClient = (language = "nb", versionHash?: string) => {
export const createApolloClient = (language = "nb", versionHash?: string, path?: string) => {
const cache = getCache();

return new ApolloClient({
link: createApolloLinks(language, versionHash),
link: createApolloLinks(language, versionHash, path),
cache,
});
};

export const createApolloLinks = (lang: string, versionHash?: string) => {
const getLogLevel = (extensions: GraphQLErrorExtensions): LogLevel => {
if (typeof extensions?.status === "number" && extensions.status < 500) {
return "warn";
}
return "error";
};

export const createApolloLinks = (lang: string, versionHash?: string, requestPath?: string) => {
const cookieString = config.isClient ? document.cookie : "";
const feideCookie = getFeideCookie(cookieString);
const accessTokenValid = isAccessTokenValid(feideCookie);
Expand All @@ -221,26 +229,31 @@ export const createApolloLinks = (lang: string, versionHash?: string) => {
},
};
});
return ApolloLink.from([
onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path, extensions }) => {
if (!config.isClient || extensions?.status !== 404) {
handleError(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`);
}
});
}
if (networkError) {
handleError(`[Network error]: ${networkError}`, {
clientTime: new Date(),
});
}
}),
headersLink,
new BatchHttpLink({
uri,
}),
]);

const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path, extensions }) => {
if (!config.isClient || extensions?.status !== 404) {
handleError(`[GraphQL error]: ${message}`, undefined, requestPath, getLogLevel(extensions), {
requestPath,
graphqlError: {
message,
locations,
path,
extensions,
},
});
}
});
}
if (networkError) {
handleError(`[Network error]: ${networkError}`, {
clientTime: new Date(),
});
}
});

return ApolloLink.from([errorLink, headersLink, new BatchHttpLink({ uri })]);
};

type HttpHeaders = {
Expand Down
46 changes: 39 additions & 7 deletions src/util/handleError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
*
*/

import { Request } from "express";
import { ErrorInfo } from "react";
import { ApolloError } from "@apollo/client";
import { ErrorReporter } from "@ndla/error-reporter";
Expand Down Expand Up @@ -70,27 +69,60 @@ export const isInternalServerError = (error: ApolloError | undefined | null): bo

const getErrorLog = (
error: ApolloError | Error | string | unknown,
request?: Request,
extraContext: Record<string, unknown>,
): ApolloError | Error | string | unknown => {
if (typeof error === "object") {
const errWithPath = error as { requestPath?: string } & object;
errWithPath.requestPath = request?.path;
return errWithPath;
return { ...errWithPath, ...extraContext };
}

if (typeof error === "string") {
return { message: error, ...extraContext };
}

return error;
};

export type LogLevel = "error" | "warn" | "info";
const unreachable = (parameter: never): never => {
throw new Error(`This code should be unreachable but is not, because '${parameter}' is not of 'never' type.`);
};

const logServerError = async (
error: ApolloError | Error | string | unknown,
requestPath: string | undefined,
loglevel: LogLevel | undefined,
extraContext: Record<string, unknown>,
) => {
const ctx = { ...extraContext, requestPath };
const err = getErrorLog(error, ctx);
switch (loglevel) {
case "info":
await log?.info(err);
break;
case "warn":
await log?.warn(err);
break;
case "error":
case undefined:
await log?.error(err);
break;
default:
unreachable(loglevel);
}
};

const handleError = async (
error: ApolloError | Error | string | unknown,
info?: ErrorInfo | { clientTime: Date },
request?: Request,
requestPath?: string,
loglevel?: LogLevel,
extraContext: Record<string, unknown> = {},
) => {
if (config.runtimeType === "production" && config.isClient) {
ErrorReporter.getInstance().captureError(error, info);
} else if (config.runtimeType === "production" && !config.isClient) {
const err = getErrorLog(error, request);
await log?.error(err);
await logServerError(error, requestPath, loglevel, extraContext);
} else {
console.error(error); // eslint-disable-line no-console
}
Expand Down

0 comments on commit 3fe773c

Please sign in to comment.