Skip to content

Commit 3e490f9

Browse files
committed
fix(backend): log all errors to sentry
1 parent 2d7747e commit 3e490f9

10 files changed

Lines changed: 70 additions & 62 deletions

File tree

apps/backend/src/app.ts

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
/* eslint-disable @typescript-eslint/max-params */
2-
import * as Sentry from '@sentry/node';
32
import cors from '@fastify/cors';
43

54
import { routes } from './routes/index.js';
65
import { notFound, serverError } from './utils/apiErrors.js';
7-
import { envs } from './utils/env.js';
8-
import { defaultLogger as logger } from './utils/logger.js';
6+
import { defaultLogger as logger, logError } from './utils/logger.js';
97

108
import type { FastifyInstance, FastifyPluginOptions, FastifyServerOptions } from 'fastify';
119

@@ -36,22 +34,16 @@ export default async function createApp(
3634
});
3735

3836
f.setErrorHandler(function (error, req, res) {
39-
logger.error(error instanceof Error ? error.message : error);
40-
41-
// Capture error in Sentry
42-
if (envs.SENTRY_DSN && error instanceof Error) {
43-
Sentry.captureException(error, {
44-
tags: {
45-
route: req.url,
46-
method: req.method,
47-
},
48-
contexts: {
49-
request: {
50-
method: req.method,
51-
url: req.url,
52-
headers: req.headers,
53-
},
54-
},
37+
// logError will handle both local logging and Sentry in production
38+
if (error instanceof Error) {
39+
logError(error, null, {
40+
route: req.url,
41+
method: req.method,
42+
});
43+
} else {
44+
logError(new Error('unknown error in API'), error, {
45+
route: req.url,
46+
method: req.method,
5547
});
5648
}
5749

apps/backend/src/db/migration.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { FileMigrationProvider, Migrator } from 'kysely';
66
import { db, kyselyClickhouse } from './client.js';
77
import { algolia } from '../utils/algolia.js';
88
import { envs } from '../utils/env.js';
9-
import { defaultLogger as logger } from '../utils/logger.js';
9+
import { defaultLogger as logger, logError } from '../utils/logger.js';
1010

1111
import type { IndexSettings } from 'algoliasearch';
1212

@@ -31,8 +31,7 @@ export async function migrate(): Promise<boolean> {
3131
{
3232
const { error, results } = await migratorDB.migrateToLatest();
3333
if (error !== undefined) {
34-
logger.error('failed to migrate DN');
35-
logger.error(error);
34+
logError(new Error('failed to migrate DN'), error);
3635
return false;
3736
}
3837

@@ -41,7 +40,7 @@ export async function migrate(): Promise<boolean> {
4140
if (it.status === 'Success') {
4241
logger.info(`migration "${it.migrationName}" was executed successfully`);
4342
} else if (it.status === 'Error') {
44-
logger.error(`failed to execute migration "${it.migrationName}"`);
43+
logError(new Error(`failed to execute migration "${it.migrationName}"`));
4544
}
4645
}
4746
}
@@ -51,8 +50,7 @@ export async function migrate(): Promise<boolean> {
5150
{
5251
const { error, results } = await migratorClickhouse.migrateToLatest();
5352
if (error !== undefined) {
54-
logger.error('failed to migrate ClickHouse');
55-
logger.error(error);
53+
logError(new Error('failed to migrate ClickHouse'), error);
5654
return false;
5755
}
5856

@@ -61,7 +59,7 @@ export async function migrate(): Promise<boolean> {
6159
if (it.status === 'Success') {
6260
logger.info(`migration "${it.migrationName}" was executed successfully`);
6361
} else if (it.status === 'Error') {
64-
logger.error(`failed to execute migration "${it.migrationName}"`);
62+
logError(new Error(`failed to execute migration "${it.migrationName}"`));
6563
}
6664
}
6765
}

apps/backend/src/index.ts

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import Fastify from 'fastify';
55

66
import createApp, { options } from './app.js';
77
import { envs } from './utils/env.js';
8-
import { defaultLogger as logger } from './utils/logger.js';
8+
import { defaultLogger as logger, logError } from './utils/logger.js';
99

1010
import './processor/cronAnalyzer.js';
1111
import './processor/cronList.js';
@@ -31,24 +31,16 @@ void app.register(createApp);
3131

3232
process
3333
.on('unhandledRejection', (reason) => {
34-
logger.error('Unhandled Rejection at Promise', reason);
34+
logError(new Error('Unhandled Rejection at Promise'), reason);
35+
// Flush Sentry before exiting in production
3536
if (envs.SENTRY_DSN) {
36-
Sentry.captureException(reason instanceof Error ? reason : new Error(String(reason)), {
37-
tags: {
38-
errorType: 'unhandledRejection',
39-
},
40-
});
37+
void Sentry.flush(2000);
4138
}
4239
})
4340
.on('uncaughtException', (err) => {
44-
logger.error('Uncaught Exception thrown', err);
41+
logError(new Error('Uncaught Exception thrown'), err);
42+
// Flush Sentry before exiting in production
4543
if (envs.SENTRY_DSN) {
46-
Sentry.captureException(err, {
47-
tags: {
48-
errorType: 'uncaughtException',
49-
},
50-
});
51-
// Flush Sentry before exiting
5244
void Sentry.flush(2000).then(() => {
5345
process.exit(1);
5446
});
@@ -71,7 +63,7 @@ app.addHook('onClose', async (instance) => {
7163
closeListeners.uninstall();
7264
await instance.close();
7365
} catch (err) {
74-
logger.error(err);
66+
logError(new Error('Failed to close'), err);
7567
}
7668
});
7769

apps/backend/src/models/repositories.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { clickHouse, db } from '../db/client.js';
22
import { algolia } from '../utils/algolia.js';
33
import { formatToClickhouseDatetime } from '../utils/date.js';
44
import { envs } from '../utils/env.js';
5+
import { logError } from '../utils/logger.js';
56

67
import type { ClickhouseRepositoryInsert } from '../db/types.clickhouse.js';
78
import type { RepositoryInsert, RepositoryRow, RepositoryUpdate, TX } from '../db/types.db.js';
@@ -174,7 +175,7 @@ export async function reindexRepositoriesToAlgolia(logger: Logger): Promise<void
174175
logger.info(`Reindexed ${success} repositories...`);
175176
} catch (err: unknown) {
176177
failed += objects.length;
177-
logger.error(`Failed to reindex chunk:`, err);
178+
logError(new Error('Failed to reindex chunk'), err);
178179
}
179180
}
180181

apps/backend/src/processor/cronAnalyzer.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { getOrInsert, update } from '../models/progress.js';
88
import { getRepositoryToAnalyze, updateRepository } from '../models/repositories.js';
99
import { formatToYearWeek } from '../utils/date.js';
1010
import { envs } from '../utils/env.js';
11-
import { defaultLogger } from '../utils/logger.js';
11+
import { defaultLogger, logError } from '../utils/logger.js';
1212
import { wait } from '../utils/wait.js';
1313

1414
import type { Payload } from '@specfy/stack-analyser';
@@ -53,14 +53,14 @@ export const cronAnalyzeGithubRepositories = CronJob.from({
5353
return true;
5454
}
5555
} catch (err) {
56-
logger.error(err, 'Failed to get previous');
56+
logError(new Error('Failed to get previous'), err);
5757
}
5858

5959
let res: Payload;
6060
try {
6161
res = await analyze(repo, logger);
6262
} catch (err) {
63-
logger.error(err, `Failed to analyze`);
63+
logError(new Error(`Failed to analyze`), err);
6464
await updateRepository({ trx, id: repo.id, input: { errored: 1 } });
6565
await wait(envs.ANALYZE_WAIT);
6666
return true;
@@ -70,7 +70,7 @@ export const cronAnalyzeGithubRepositories = CronJob.from({
7070
await saveAnalysis({ trx, repo, res, dateWeek });
7171
logger.info(`Done`);
7272
} catch (err) {
73-
logger.error(err, `Failed to save`);
73+
logError(new Error(`Failed to save`), err);
7474
await updateRepository({ trx, id: repo.id, input: { errored: 1 } });
7575
}
7676

apps/backend/src/processor/cronList.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { algolia } from '../utils/algolia.js';
1111
import { formatToClickhouseDatetime, formatToDate, formatToYearWeek } from '../utils/date.js';
1212
import { envs } from '../utils/env.js';
1313
import { octokit } from '../utils/github.js';
14-
import { defaultLogger } from '../utils/logger.js';
14+
import { defaultLogger, logError } from '../utils/logger.js';
1515
import { wait } from '../utils/wait.js';
1616

1717
import type { AlgoliaRepositoryObject } from '../types/algolia.js';
@@ -62,7 +62,7 @@ export const cronListGithubRepositories = CronJob.from({
6262
});
6363
}
6464
} catch (err) {
65-
logger.error(err, 'Error fetching repositories from GitHub:');
65+
logError(new Error('Error fetching repositories from GitHub'), err);
6666
}
6767

6868
if (currentDate.getTime() <= endDate.getTime()) {

apps/backend/src/routes/v1/repositories/$org/$name/postAnalyzeOne.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { getRepository } from '../../../../../models/repositories.js';
55
import { analyze, saveAnalysis, savePreviousIfStale } from '../../../../../processor/analyzer.js';
66
import { notFound } from '../../../../../utils/apiErrors.js';
77
import { formatToYearWeek } from '../../../../../utils/date.js';
8-
import { defaultLogger } from '../../../../../utils/logger.js';
8+
import { defaultLogger, logError } from '../../../../../utils/logger.js';
99

1010
import type { FastifyInstance, FastifyPluginCallback } from 'fastify';
1111

@@ -45,7 +45,7 @@ export const postAnalyzeOne: FastifyPluginCallback = (fastify: FastifyInstance)
4545
}
4646
});
4747
} catch (err) {
48-
logger.error(`Failed to process`, err);
48+
logError(new Error('Failed to process'), err);
4949
reply.status(500).send({ error: { code: 'failed_to_process', status: 500 } });
5050
return;
5151
}

apps/backend/src/scripts/blogIndex.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// @ts-nocheck in dockerfile it's not present
33
import { allPosts } from '../../../frontend/.content-collections/generated/index.js';
44
import { db } from '../db/client.js';
5-
import { defaultLogger } from '../utils/logger.js';
5+
import { defaultLogger, logError } from '../utils/logger.js';
66

77
import type { PostsInsert } from '../db/types.db.js';
88

@@ -62,7 +62,7 @@ async function main(): Promise<void> {
6262

6363
logger.info('Finished indexing all posts');
6464
} catch (err) {
65-
logger.error('Failed to index posts', err);
65+
logError(new Error('Failed to index posts'), err);
6666
throw err;
6767
}
6868
}

apps/backend/src/scripts/warmupCache.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { listTech } from '@specfy/stack-analyser/dist/common/techs.generated.js';
22

33
import { listAllRepositories } from '../models/repositories.js';
4-
import { defaultLogger } from '../utils/logger.js';
4+
import { defaultLogger, logError } from '../utils/logger.js';
55
import { categories } from '../utils/stacks.js';
66
import { wait } from '../utils/wait.js';
77

@@ -32,15 +32,15 @@ if (hasCategories) {
3232

3333
const res = await fetch(url);
3434
if (!res.ok) {
35-
logger.error(`Error: status ${res.status}`);
35+
logError(new Error(`Error: status ${res.status}`));
3636
}
3737

3838
const resLeaderboard = await fetch(`${url}/leaderboard`);
3939
if (!resLeaderboard.ok) {
40-
logger.error(`Error leaderboard: status ${resLeaderboard.status}`);
40+
logError(new Error(`Error leaderboard: status ${resLeaderboard.status}`));
4141
}
4242
} catch (err) {
43-
logger.error(`Error:`, err);
43+
logError(new Error('Failed to warm up cache'), err);
4444
}
4545
await wait(100);
4646
}
@@ -57,14 +57,14 @@ if (hasTechs) {
5757
logger.info(`Tech: ${tech.key}`);
5858
const res = await fetch(url);
5959
if (!res.ok) {
60-
logger.error(`Error: status ${res.status}`);
60+
logError(new Error(`Error: status ${res.status}`));
6161
}
6262
const resRelated = await fetch(`${url}/related`);
6363
if (!resRelated.ok) {
64-
logger.error(`Error related: status ${resRelated.status}`);
64+
logError(new Error(`Error related: status ${resRelated.status}`));
6565
}
6666
} catch (err) {
67-
logger.error(`Error:`, err);
67+
logError(new Error(`Failed to fetch tech`), err);
6868
}
6969
await wait(100);
7070
techCount++;
@@ -88,10 +88,10 @@ if (hasRepos) {
8888
logger.info(`Repo: ${repo.org}/${repo.name}`);
8989
const res = await fetch(url);
9090
if (!res.ok) {
91-
logger.error(`Error: status ${res.status}`);
91+
logError(new Error(`Error: status ${res.status}`));
9292
}
9393
} catch (err) {
94-
logger.error(`Error:`, err);
94+
logError(new Error(`Failed to fetch repo`), err);
9595
}
9696
await wait(100);
9797
repoCount++;

apps/backend/src/utils/logger.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import * as Sentry from '@sentry/node';
12
import { pino } from 'pino';
23

3-
import { envs } from './env.js';
4+
import { envs, isProd } from './env.js';
45

56
import type { LoggerOptions } from 'pino';
67

@@ -63,3 +64,27 @@ if (envs.IS_PROD && options.formatters) {
6364

6465
export const defaultLogger = pino(options);
6566
export type Logger = typeof defaultLogger;
67+
68+
/**
69+
* Logs an error locally and sends to Sentry in production.
70+
* Always logs locally for debugging, and sends to Sentry in production.
71+
*/
72+
export function logError(message: Error, err?: unknown, extra?: Record<string, unknown>): void {
73+
// Always log locally
74+
// @ts-expect-error - pino logger accepts flexible arguments
75+
defaultLogger.error(...args);
76+
77+
// In production, also send to Sentry
78+
if (isProd && envs.SENTRY_DSN) {
79+
80+
const context: { level?: 'error'; extra?: Record<string, unknown> } = { level: 'error' };
81+
if (extra && Object.keys(extra).length > 0) {
82+
context.extra = extra;
83+
}
84+
if (err) {
85+
message.cause = err
86+
}
87+
88+
Sentry.captureException(message, Object.keys(context).length > 0 ? context : undefined);
89+
}
90+
}

0 commit comments

Comments
 (0)