mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 12:25:22 +00:00
add sentry support to backend and webapp (#223)
* add sentry to web app * set sentry environemnt from env var * add sentry env replace logic in docker container * wip add backend sentry * add sentry to backend * move dns to env var * remove test exception
This commit is contained in:
parent
85c21a2519
commit
a93ee6527c
25 changed files with 1180 additions and 32 deletions
|
|
@ -48,6 +48,8 @@ ARG NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED=BAKED_NEXT_PUBLIC_SOURCEBOT_TELEMET
|
||||||
ARG NEXT_PUBLIC_SOURCEBOT_VERSION=BAKED_NEXT_PUBLIC_SOURCEBOT_VERSION
|
ARG NEXT_PUBLIC_SOURCEBOT_VERSION=BAKED_NEXT_PUBLIC_SOURCEBOT_VERSION
|
||||||
ENV NEXT_PUBLIC_POSTHOG_PAPIK=BAKED_NEXT_PUBLIC_POSTHOG_PAPIK
|
ENV NEXT_PUBLIC_POSTHOG_PAPIK=BAKED_NEXT_PUBLIC_POSTHOG_PAPIK
|
||||||
ENV NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=BAKED_NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
|
ENV NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=BAKED_NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
|
||||||
|
ENV NEXT_PUBLIC_SENTRY_ENVIRONMENT=BAKED_NEXT_PUBLIC_SENTRY_ENVIRONMENT
|
||||||
|
ENV NEXT_PUBLIC_SENTRY_WEBAPP_DSN=BAKED_NEXT_PUBLIC_SENTRY_WEBAPP_DSN
|
||||||
|
|
||||||
# @nocheckin: This was interfering with the the `matcher` regex in middleware.ts,
|
# @nocheckin: This was interfering with the the `matcher` regex in middleware.ts,
|
||||||
# causing regular expressions parsing errors when making a request. It's unclear
|
# causing regular expressions parsing errors when making a request. It's unclear
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,12 @@ echo "{\"version\": \"$SOURCEBOT_VERSION\", \"install_id\": \"$SOURCEBOT_INSTALL
|
||||||
# Always infer NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
|
# Always infer NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
|
||||||
export NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="$STRIPE_PUBLISHABLE_KEY"
|
export NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="$STRIPE_PUBLISHABLE_KEY"
|
||||||
|
|
||||||
|
# Always infer NEXT_PUBLIC_SENTRY_ENVIRONMENT
|
||||||
|
export NEXT_PUBLIC_SENTRY_ENVIRONMENT="$SENTRY_ENVIRONMENT"
|
||||||
|
|
||||||
|
# Always infer NEXT_PUBLIC_SENTRY_WEBAPP_DSN
|
||||||
|
export NEXT_PUBLIC_SENTRY_WEBAPP_DSN="$SENTRY_WEBAPP_DSN"
|
||||||
|
|
||||||
# Iterate over all .js files in .next & public, making substitutions for the `BAKED_` sentinal values
|
# Iterate over all .js files in .next & public, making substitutions for the `BAKED_` sentinal values
|
||||||
# with their actual desired runtime value.
|
# with their actual desired runtime value.
|
||||||
find /app/packages/web/public /app/packages/web/.next -type f -name "*.js" |
|
find /app/packages/web/public /app/packages/web/.next -type f -name "*.js" |
|
||||||
|
|
@ -131,6 +137,8 @@ echo "{\"version\": \"$SOURCEBOT_VERSION\", \"install_id\": \"$SOURCEBOT_INSTALL
|
||||||
sed -i "s|BAKED_NEXT_PUBLIC_SOURCEBOT_VERSION|${NEXT_PUBLIC_SOURCEBOT_VERSION}|g" "$file"
|
sed -i "s|BAKED_NEXT_PUBLIC_SOURCEBOT_VERSION|${NEXT_PUBLIC_SOURCEBOT_VERSION}|g" "$file"
|
||||||
sed -i "s|BAKED_NEXT_PUBLIC_POSTHOG_PAPIK|${NEXT_PUBLIC_POSTHOG_PAPIK}|g" "$file"
|
sed -i "s|BAKED_NEXT_PUBLIC_POSTHOG_PAPIK|${NEXT_PUBLIC_POSTHOG_PAPIK}|g" "$file"
|
||||||
sed -i "s|BAKED_NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY|${NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY}|g" "$file"
|
sed -i "s|BAKED_NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY|${NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY}|g" "$file"
|
||||||
|
sed -i "s|BAKED_NEXT_PUBLIC_SENTRY_ENVIRONMENT|${NEXT_PUBLIC_SENTRY_ENVIRONMENT}|g" "$file"
|
||||||
|
sed -i "s|BAKED_NEXT_PUBLIC_SENTRY_WEBAPP_DSN|${NEXT_PUBLIC_SENTRY_WEBAPP_DSN}|g" "$file"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
2
packages/backend/.gitignore
vendored
2
packages/backend/.gitignore
vendored
|
|
@ -1,2 +1,4 @@
|
||||||
dist/
|
dist/
|
||||||
!.env
|
!.env
|
||||||
|
# Sentry Config File
|
||||||
|
.sentryclirc
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev:watch": "tsc-watch --preserveWatchOutput --onSuccess \"yarn dev --cacheDir ../../.sourcebot\"",
|
"dev:watch": "tsc-watch --preserveWatchOutput --onSuccess \"yarn dev --cacheDir ../../.sourcebot\"",
|
||||||
"dev": "export PATH=\"$PWD/../../bin:$PATH\" && export CTAGS_COMMAND=ctags && node ./dist/index.js",
|
"dev": "export PATH=\"$PWD/../../bin:$PATH\" && export CTAGS_COMMAND=ctags && node ./dist/index.js",
|
||||||
"build": "tsc",
|
"build": "tsc && yarn sentry:sourcemaps",
|
||||||
"test": "vitest --config ./vitest.config.ts"
|
"test": "vitest --config ./vitest.config.ts",
|
||||||
|
"sentry:sourcemaps": "sentry-cli sourcemaps inject --org sourcebot --project backend ./dist && sentry-cli sourcemaps upload --org sourcebot --project backend ./dist"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/argparse": "^2.0.16",
|
"@types/argparse": "^2.0.16",
|
||||||
|
|
@ -23,6 +24,9 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gitbeaker/rest": "^40.5.1",
|
"@gitbeaker/rest": "^40.5.1",
|
||||||
"@octokit/rest": "^21.0.2",
|
"@octokit/rest": "^21.0.2",
|
||||||
|
"@sentry/cli": "^2.42.2",
|
||||||
|
"@sentry/node": "^9.3.0",
|
||||||
|
"@sentry/profiling-node": "^9.3.0",
|
||||||
"@sourcebot/crypto": "^0.1.0",
|
"@sourcebot/crypto": "^0.1.0",
|
||||||
"@sourcebot/db": "^0.1.0",
|
"@sourcebot/db": "^0.1.0",
|
||||||
"@sourcebot/error": "^0.1.0",
|
"@sourcebot/error": "^0.1.0",
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { Redis } from 'ioredis';
|
||||||
import { RepoData, compileGithubConfig, compileGitlabConfig, compileGiteaConfig, compileGerritConfig } from "./repoCompileUtils.js";
|
import { RepoData, compileGithubConfig, compileGitlabConfig, compileGiteaConfig, compileGerritConfig } from "./repoCompileUtils.js";
|
||||||
import { BackendError, BackendException } from "@sourcebot/error";
|
import { BackendError, BackendException } from "@sourcebot/error";
|
||||||
import { captureEvent } from "./posthog.js";
|
import { captureEvent } from "./posthog.js";
|
||||||
|
import * as Sentry from "@sentry/node";
|
||||||
|
|
||||||
interface IConnectionManager {
|
interface IConnectionManager {
|
||||||
scheduleConnectionSync: (connection: Connection) => Promise<void>;
|
scheduleConnectionSync: (connection: Connection) => Promise<void>;
|
||||||
|
|
@ -94,9 +95,11 @@ export class ConnectionManager implements IConnectionManager {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
throw new BackendException(BackendError.CONNECTION_SYNC_CONNECTION_NOT_FOUND, {
|
const e = new BackendException(BackendError.CONNECTION_SYNC_CONNECTION_NOT_FOUND, {
|
||||||
message: `Connection ${job.data.connectionId} not found`,
|
message: `Connection ${job.data.connectionId} not found`,
|
||||||
});
|
});
|
||||||
|
Sentry.captureException(e);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset the syncStatusMetadata to an empty object at the start of the sync job
|
// Reset the syncStatusMetadata to an empty object at the start of the sync job
|
||||||
|
|
@ -146,6 +149,8 @@ export class ConnectionManager implements IConnectionManager {
|
||||||
})();
|
})();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(`Failed to compile repo data for connection ${job.data.connectionId}: ${err}`);
|
this.logger.error(`Failed to compile repo data for connection ${job.data.connectionId}: ${err}`);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
|
||||||
if (err instanceof BackendException) {
|
if (err instanceof BackendException) {
|
||||||
throw err;
|
throw err;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import * as Sentry from "@sentry/node";
|
||||||
|
|
||||||
type ValidResult<T> = {
|
type ValidResult<T> = {
|
||||||
type: 'valid';
|
type: 'valid';
|
||||||
data: T[];
|
data: T[];
|
||||||
|
|
@ -39,6 +41,7 @@ export function processPromiseResults<T>(
|
||||||
export function throwIfAnyFailed<T>(results: PromiseSettledResult<T>[]) {
|
export function throwIfAnyFailed<T>(results: PromiseSettledResult<T>[]) {
|
||||||
const failedResult = results.find(result => result.status === 'rejected');
|
const failedResult = results.find(result => result.status === 'rejected');
|
||||||
if (failedResult) {
|
if (failedResult) {
|
||||||
|
Sentry.captureException(failedResult.reason);
|
||||||
throw failedResult.reason;
|
throw failedResult.reason;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
|
import * as Sentry from "@sentry/node";
|
||||||
|
|
||||||
export const getEnv = (env: string | undefined, defaultValue?: string, required?: boolean) => {
|
export const getEnv = (env: string | undefined, defaultValue?: string, required?: boolean) => {
|
||||||
if (required && !env && !defaultValue) {
|
if (required && !env && !defaultValue) {
|
||||||
throw new Error(`Missing required environment variable: ${env}`);
|
const e = new Error(`Missing required environment variable: ${env}`);
|
||||||
|
Sentry.captureException(e);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
return env ?? defaultValue;
|
return env ?? defaultValue;
|
||||||
|
|
@ -37,3 +40,6 @@ export const FALLBACK_GITEA_TOKEN = getEnv(process.env.FALLBACK_GITEA_TOKEN);
|
||||||
|
|
||||||
export const INDEX_CONCURRENCY_MULTIPLE = getEnv(process.env.INDEX_CONCURRENCY_MULTIPLE);
|
export const INDEX_CONCURRENCY_MULTIPLE = getEnv(process.env.INDEX_CONCURRENCY_MULTIPLE);
|
||||||
export const REDIS_URL = getEnv(process.env.REDIS_URL, 'redis://localhost:6379')!;
|
export const REDIS_URL = getEnv(process.env.REDIS_URL, 'redis://localhost:6379')!;
|
||||||
|
|
||||||
|
export const SENTRY_BACKEND_DSN = getEnv(process.env.SENTRY_BACKEND_DSN);
|
||||||
|
export const SENTRY_ENVIRONMENT = getEnv(process.env.SENTRY_ENVIRONMENT, 'unknown')!;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import micromatch from "micromatch";
|
||||||
import { measure, fetchWithRetry } from './utils.js';
|
import { measure, fetchWithRetry } from './utils.js';
|
||||||
import { BackendError } from '@sourcebot/error';
|
import { BackendError } from '@sourcebot/error';
|
||||||
import { BackendException } from '@sourcebot/error';
|
import { BackendException } from '@sourcebot/error';
|
||||||
|
import * as Sentry from "@sentry/node";
|
||||||
|
|
||||||
// https://gerrit-review.googlesource.com/Documentation/rest-api.html
|
// https://gerrit-review.googlesource.com/Documentation/rest-api.html
|
||||||
interface GerritProjects {
|
interface GerritProjects {
|
||||||
|
|
@ -40,6 +41,7 @@ export const getGerritReposFromConfig = async (config: GerritConfig): Promise<Ge
|
||||||
const fetchFn = () => fetchAllProjects(url);
|
const fetchFn = () => fetchAllProjects(url);
|
||||||
return fetchWithRetry(fetchFn, `projects from ${url}`, logger);
|
return fetchWithRetry(fetchFn, `projects from ${url}`, logger);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
Sentry.captureException(err);
|
||||||
if (err instanceof BackendException) {
|
if (err instanceof BackendException) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
@ -50,7 +52,9 @@ export const getGerritReposFromConfig = async (config: GerritConfig): Promise<Ge
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!projects) {
|
if (!projects) {
|
||||||
throw new Error(`Failed to fetch projects from ${url}`);
|
const e = new Error(`Failed to fetch projects from ${url}`);
|
||||||
|
Sentry.captureException(e);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
// exclude "All-Projects" and "All-Users" projects
|
// exclude "All-Projects" and "All-Users" projects
|
||||||
|
|
@ -89,11 +93,14 @@ const fetchAllProjects = async (url: string): Promise<GerritProject[]> => {
|
||||||
response = await fetch(endpointWithParams);
|
response = await fetch(endpointWithParams);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.log(`Failed to fetch projects from Gerrit at ${endpointWithParams} with status ${response.status}`);
|
console.log(`Failed to fetch projects from Gerrit at ${endpointWithParams} with status ${response.status}`);
|
||||||
throw new BackendException(BackendError.CONNECTION_SYNC_FAILED_TO_FETCH_GERRIT_PROJECTS, {
|
const e = new BackendException(BackendError.CONNECTION_SYNC_FAILED_TO_FETCH_GERRIT_PROJECTS, {
|
||||||
status: response.status,
|
status: response.status,
|
||||||
});
|
});
|
||||||
|
Sentry.captureException(e);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
Sentry.captureException(err);
|
||||||
if (err instanceof BackendException) {
|
if (err instanceof BackendException) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import micromatch from 'micromatch';
|
||||||
import { PrismaClient } from '@sourcebot/db';
|
import { PrismaClient } from '@sourcebot/db';
|
||||||
import { FALLBACK_GITEA_TOKEN } from './environment.js';
|
import { FALLBACK_GITEA_TOKEN } from './environment.js';
|
||||||
import { processPromiseResults, throwIfAnyFailed } from './connectionUtils.js';
|
import { processPromiseResults, throwIfAnyFailed } from './connectionUtils.js';
|
||||||
|
import * as Sentry from "@sentry/node";
|
||||||
|
|
||||||
const logger = createLogger('Gitea');
|
const logger = createLogger('Gitea');
|
||||||
|
|
||||||
export const getGiteaReposFromConfig = async (config: GiteaConnectionConfig, orgId: number, db: PrismaClient) => {
|
export const getGiteaReposFromConfig = async (config: GiteaConnectionConfig, orgId: number, db: PrismaClient) => {
|
||||||
|
|
@ -132,6 +134,8 @@ const getReposOwnedByUsers = async <T>(users: string[], api: Api<T>) => {
|
||||||
data
|
data
|
||||||
};
|
};
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
Sentry.captureException(e);
|
||||||
|
|
||||||
if (e?.status === 404) {
|
if (e?.status === 404) {
|
||||||
logger.error(`User ${user} not found or no access`);
|
logger.error(`User ${user} not found or no access`);
|
||||||
return {
|
return {
|
||||||
|
|
@ -170,6 +174,8 @@ const getReposForOrgs = async <T>(orgs: string[], api: Api<T>) => {
|
||||||
data
|
data
|
||||||
};
|
};
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
Sentry.captureException(e);
|
||||||
|
|
||||||
if (e?.status === 404) {
|
if (e?.status === 404) {
|
||||||
logger.error(`Organization ${org} not found or no access`);
|
logger.error(`Organization ${org} not found or no access`);
|
||||||
return {
|
return {
|
||||||
|
|
@ -206,6 +212,8 @@ const getRepos = async <T>(repos: string[], api: Api<T>) => {
|
||||||
data: [response.data]
|
data: [response.data]
|
||||||
};
|
};
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
Sentry.captureException(e);
|
||||||
|
|
||||||
if (e?.status === 404) {
|
if (e?.status === 404) {
|
||||||
logger.error(`Repository ${repo} not found or no access`);
|
logger.error(`Repository ${repo} not found or no access`);
|
||||||
return {
|
return {
|
||||||
|
|
@ -234,7 +242,9 @@ const paginate = async <T>(request: (page: number) => Promise<HttpResponse<T[],
|
||||||
|
|
||||||
const totalCountString = result.headers.get('x-total-count');
|
const totalCountString = result.headers.get('x-total-count');
|
||||||
if (!totalCountString) {
|
if (!totalCountString) {
|
||||||
throw new Error("Header 'x-total-count' not found");
|
const e = new Error("Header 'x-total-count' not found");
|
||||||
|
Sentry.captureException(e);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
const totalCount = parseInt(totalCountString);
|
const totalCount = parseInt(totalCountString);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import { PrismaClient } from "@sourcebot/db";
|
||||||
import { FALLBACK_GITHUB_TOKEN } from "./environment.js";
|
import { FALLBACK_GITHUB_TOKEN } from "./environment.js";
|
||||||
import { BackendException, BackendError } from "@sourcebot/error";
|
import { BackendException, BackendError } from "@sourcebot/error";
|
||||||
import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js";
|
import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js";
|
||||||
|
import * as Sentry from "@sentry/node";
|
||||||
|
|
||||||
const logger = createLogger("GitHub");
|
const logger = createLogger("GitHub");
|
||||||
|
|
||||||
export type OctokitRepository = {
|
export type OctokitRepository = {
|
||||||
|
|
@ -53,15 +55,21 @@ export const getGitHubReposFromConfig = async (config: GithubConnectionConfig, o
|
||||||
try {
|
try {
|
||||||
await octokit.rest.users.getAuthenticated();
|
await octokit.rest.users.getAuthenticated();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
Sentry.captureException(error);
|
||||||
|
|
||||||
if (isHttpError(error, 401)) {
|
if (isHttpError(error, 401)) {
|
||||||
throw new BackendException(BackendError.CONNECTION_SYNC_INVALID_TOKEN, {
|
const e = new BackendException(BackendError.CONNECTION_SYNC_INVALID_TOKEN, {
|
||||||
secretKey,
|
secretKey,
|
||||||
});
|
});
|
||||||
|
Sentry.captureException(e);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new BackendException(BackendError.CONNECTION_SYNC_SYSTEM_ERROR, {
|
const e = new BackendException(BackendError.CONNECTION_SYNC_SYSTEM_ERROR, {
|
||||||
message: `Failed to authenticate with GitHub`,
|
message: `Failed to authenticate with GitHub`,
|
||||||
});
|
});
|
||||||
|
Sentry.captureException(e);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -239,6 +247,8 @@ const getReposOwnedByUsers = async (users: string[], isAuthenticated: boolean, o
|
||||||
data
|
data
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
Sentry.captureException(error);
|
||||||
|
|
||||||
if (isHttpError(error, 404)) {
|
if (isHttpError(error, 404)) {
|
||||||
logger.error(`User ${user} not found or no access`);
|
logger.error(`User ${user} not found or no access`);
|
||||||
return {
|
return {
|
||||||
|
|
@ -282,6 +292,8 @@ const getReposForOrgs = async (orgs: string[], octokit: Octokit, signal: AbortSi
|
||||||
data
|
data
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
Sentry.captureException(error);
|
||||||
|
|
||||||
if (isHttpError(error, 404)) {
|
if (isHttpError(error, 404)) {
|
||||||
logger.error(`Organization ${org} not found or no access`);
|
logger.error(`Organization ${org} not found or no access`);
|
||||||
return {
|
return {
|
||||||
|
|
@ -327,6 +339,8 @@ const getRepos = async (repoList: string[], octokit: Octokit, signal: AbortSigna
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
Sentry.captureException(error);
|
||||||
|
|
||||||
if (isHttpError(error, 404)) {
|
if (isHttpError(error, 404)) {
|
||||||
logger.error(`Repository ${repo} not found or no access`);
|
logger.error(`Repository ${repo} not found or no access`);
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import { getTokenFromConfig, measure, fetchWithRetry } from "./utils.js";
|
||||||
import { PrismaClient } from "@sourcebot/db";
|
import { PrismaClient } from "@sourcebot/db";
|
||||||
import { FALLBACK_GITLAB_TOKEN } from "./environment.js";
|
import { FALLBACK_GITLAB_TOKEN } from "./environment.js";
|
||||||
import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js";
|
import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js";
|
||||||
|
import * as Sentry from "@sentry/node";
|
||||||
|
|
||||||
const logger = createLogger("GitLab");
|
const logger = createLogger("GitLab");
|
||||||
export const GITLAB_CLOUD_HOSTNAME = "gitlab.com";
|
export const GITLAB_CLOUD_HOSTNAME = "gitlab.com";
|
||||||
|
|
||||||
|
|
@ -47,6 +49,7 @@ export const getGitLabReposFromConfig = async (config: GitlabConnectionConfig, o
|
||||||
logger.debug(`Found ${_projects.length} projects in ${durationMs}ms.`);
|
logger.debug(`Found ${_projects.length} projects in ${durationMs}ms.`);
|
||||||
allRepos = allRepos.concat(_projects);
|
allRepos = allRepos.concat(_projects);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
Sentry.captureException(e);
|
||||||
logger.error(`Failed to fetch all projects visible in ${config.url}.`, e);
|
logger.error(`Failed to fetch all projects visible in ${config.url}.`, e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
@ -72,6 +75,8 @@ export const getGitLabReposFromConfig = async (config: GitlabConnectionConfig, o
|
||||||
data
|
data
|
||||||
};
|
};
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
Sentry.captureException(e);
|
||||||
|
|
||||||
const status = e?.cause?.response?.status;
|
const status = e?.cause?.response?.status;
|
||||||
if (status === 404) {
|
if (status === 404) {
|
||||||
logger.error(`Group ${group} not found or no access`);
|
logger.error(`Group ${group} not found or no access`);
|
||||||
|
|
@ -106,6 +111,8 @@ export const getGitLabReposFromConfig = async (config: GitlabConnectionConfig, o
|
||||||
data
|
data
|
||||||
};
|
};
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
Sentry.captureException(e);
|
||||||
|
|
||||||
const status = e?.cause?.response?.status;
|
const status = e?.cause?.response?.status;
|
||||||
if (status === 404) {
|
if (status === 404) {
|
||||||
logger.error(`User ${user} not found or no access`);
|
logger.error(`User ${user} not found or no access`);
|
||||||
|
|
@ -138,6 +145,8 @@ export const getGitLabReposFromConfig = async (config: GitlabConnectionConfig, o
|
||||||
data: [data]
|
data: [data]
|
||||||
};
|
};
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
Sentry.captureException(e);
|
||||||
|
|
||||||
const status = e?.cause?.response?.status;
|
const status = e?.cause?.response?.status;
|
||||||
|
|
||||||
if (status === 404) {
|
if (status === 404) {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
import "./instrument.js";
|
||||||
|
|
||||||
|
import * as Sentry from "@sentry/node";
|
||||||
import { ArgumentParser } from "argparse";
|
import { ArgumentParser } from "argparse";
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
import { mkdir } from 'fs/promises';
|
import { mkdir } from 'fs/promises';
|
||||||
|
|
@ -48,6 +51,8 @@ main(prisma, context)
|
||||||
})
|
})
|
||||||
.catch(async (e) => {
|
.catch(async (e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
Sentry.captureException(e);
|
||||||
|
|
||||||
await prisma.$disconnect();
|
await prisma.$disconnect();
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
})
|
})
|
||||||
|
|
|
||||||
8
packages/backend/src/instrument.ts
Normal file
8
packages/backend/src/instrument.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import * as Sentry from "@sentry/node";
|
||||||
|
import { SOURCEBOT_VERSION, SENTRY_BACKEND_DSN, SENTRY_ENVIRONMENT } from "./environment.js";
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: SENTRY_BACKEND_DSN,
|
||||||
|
release: SOURCEBOT_VERSION,
|
||||||
|
environment: SENTRY_ENVIRONMENT,
|
||||||
|
});
|
||||||
|
|
@ -10,7 +10,7 @@ import { existsSync, readdirSync, promises } from 'fs';
|
||||||
import { indexGitRepository } from "./zoekt.js";
|
import { indexGitRepository } from "./zoekt.js";
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import { PromClient } from './promClient.js';
|
import { PromClient } from './promClient.js';
|
||||||
|
import * as Sentry from "@sentry/node";
|
||||||
interface IRepoManager {
|
interface IRepoManager {
|
||||||
blockingPollLoop: () => void;
|
blockingPollLoop: () => void;
|
||||||
dispose: () => void;
|
dispose: () => void;
|
||||||
|
|
@ -258,7 +258,9 @@ export class RepoManager implements IRepoManager {
|
||||||
});
|
});
|
||||||
if (!existingRepo) {
|
if (!existingRepo) {
|
||||||
this.logger.error(`Repo ${repo.id} not found`);
|
this.logger.error(`Repo ${repo.id} not found`);
|
||||||
throw new Error(`Repo ${repo.id} not found`);
|
const e = new Error(`Repo ${repo.id} not found`);
|
||||||
|
Sentry.captureException(e);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
const repoAlreadyInIndexingState = existingRepo.repoIndexingStatus === RepoIndexingStatus.INDEXING;
|
const repoAlreadyInIndexingState = existingRepo.repoIndexingStatus === RepoIndexingStatus.INDEXING;
|
||||||
|
|
||||||
|
|
@ -287,6 +289,8 @@ export class RepoManager implements IRepoManager {
|
||||||
stats = await this.syncGitRepository(repo, repoAlreadyInIndexingState);
|
stats = await this.syncGitRepository(repo, repoAlreadyInIndexingState);
|
||||||
break;
|
break;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
Sentry.captureException(error);
|
||||||
|
|
||||||
attempts++;
|
attempts++;
|
||||||
this.promClient.repoIndexingReattemptsTotal.inc();
|
this.promClient.repoIndexingReattemptsTotal.inc();
|
||||||
if (attempts === maxAttempts) {
|
if (attempts === maxAttempts) {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { PrismaClient, Repo } from "@sourcebot/db";
|
||||||
import { decrypt } from "@sourcebot/crypto";
|
import { decrypt } from "@sourcebot/crypto";
|
||||||
import { Token } from "@sourcebot/schemas/v3/shared.type";
|
import { Token } from "@sourcebot/schemas/v3/shared.type";
|
||||||
import { BackendException, BackendError } from "@sourcebot/error";
|
import { BackendException, BackendError } from "@sourcebot/error";
|
||||||
|
import * as Sentry from "@sentry/node";
|
||||||
|
|
||||||
export const measure = async <T>(cb: () => Promise<T>) => {
|
export const measure = async <T>(cb: () => Promise<T>) => {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
|
|
@ -22,9 +23,11 @@ export const marshalBool = (value?: boolean) => {
|
||||||
|
|
||||||
export const getTokenFromConfig = async (token: Token, orgId: number, db?: PrismaClient) => {
|
export const getTokenFromConfig = async (token: Token, orgId: number, db?: PrismaClient) => {
|
||||||
if (!db) {
|
if (!db) {
|
||||||
throw new BackendException(BackendError.CONNECTION_SYNC_SYSTEM_ERROR, {
|
const e = new BackendException(BackendError.CONNECTION_SYNC_SYSTEM_ERROR, {
|
||||||
message: `No database connection provided.`,
|
message: `No database connection provided.`,
|
||||||
});
|
});
|
||||||
|
Sentry.captureException(e);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
const secretKey = token.secret;
|
const secretKey = token.secret;
|
||||||
|
|
@ -38,9 +41,11 @@ export const getTokenFromConfig = async (token: Token, orgId: number, db?: Prism
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!secret) {
|
if (!secret) {
|
||||||
throw new BackendException(BackendError.CONNECTION_SYNC_SECRET_DNE, {
|
const e = new BackendException(BackendError.CONNECTION_SYNC_SECRET_DNE, {
|
||||||
message: `Secret with key ${secretKey} not found for org ${orgId}`,
|
message: `Secret with key ${secretKey} not found for org ${orgId}`,
|
||||||
});
|
});
|
||||||
|
Sentry.captureException(e);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptedSecret = decrypt(secret.iv, secret.encryptedValue);
|
const decryptedSecret = decrypt(secret.iv, secret.encryptedValue);
|
||||||
|
|
@ -104,6 +109,8 @@ export const fetchWithRetry = async <T>(
|
||||||
try {
|
try {
|
||||||
return await fetchFn();
|
return await fetchFn();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
Sentry.captureException(e);
|
||||||
|
|
||||||
attempts++;
|
attempts++;
|
||||||
if ((e.status === 403 || e.status === 429 || e.status === 443) && attempts < maxAttempts) {
|
if ((e.status === 403 || e.status === 429 || e.status === 443) && attempts < maxAttempts) {
|
||||||
const computedWaitTime = 3000 * Math.pow(2, attempts - 1);
|
const computedWaitTime = 3000 * Math.pow(2, attempts - 1);
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,12 @@
|
||||||
"lib": ["ES2023"],
|
"lib": ["ES2023"],
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"inlineSources": true
|
"inlineSources": true,
|
||||||
|
|
||||||
|
// Set `sourceRoot` to "/" to strip the build path prefix
|
||||||
|
// from generated source code references.
|
||||||
|
// This improves issue grouping in Sentry.
|
||||||
|
"sourceRoot": "/"
|
||||||
},
|
},
|
||||||
"include": ["src/index.ts"],
|
"include": ["src/index.ts"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
|
|
|
||||||
2
packages/web/.gitignore
vendored
2
packages/web/.gitignore
vendored
|
|
@ -40,3 +40,5 @@ next-env.d.ts
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/nextjs
|
# End of https://www.toptal.com/developers/gitignore/api/nextjs
|
||||||
|
|
||||||
!.env
|
!.env
|
||||||
|
# Sentry Config File
|
||||||
|
.env.sentry-build-plugin
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import {withSentryConfig} from "@sentry/nextjs";
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
|
|
@ -41,4 +42,39 @@ const nextConfig = {
|
||||||
// } : {})
|
// } : {})
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default withSentryConfig(nextConfig, {
|
||||||
|
// For all available options, see:
|
||||||
|
// https://www.npmjs.com/package/@sentry/webpack-plugin#options
|
||||||
|
|
||||||
|
org: "sourcebot",
|
||||||
|
project: "webapp",
|
||||||
|
|
||||||
|
// Only print logs for uploading source maps in CI
|
||||||
|
silent: !process.env.CI,
|
||||||
|
|
||||||
|
// For all available options, see:
|
||||||
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
|
||||||
|
|
||||||
|
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
||||||
|
widenClientFileUpload: true,
|
||||||
|
|
||||||
|
// Automatically annotate React components to show their full name in breadcrumbs and session replay
|
||||||
|
reactComponentAnnotation: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
|
||||||
|
// This can increase your server load as well as your hosting bill.
|
||||||
|
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
|
||||||
|
// side errors will fail.
|
||||||
|
tunnelRoute: "/monitoring",
|
||||||
|
|
||||||
|
// Automatically tree-shake Sentry logger statements to reduce bundle size
|
||||||
|
disableLogger: true,
|
||||||
|
|
||||||
|
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
|
||||||
|
// See the following for more information:
|
||||||
|
// https://docs.sentry.io/product/crons/
|
||||||
|
// https://vercel.com/docs/cron-jobs
|
||||||
|
automaticVercelMonitors: true,
|
||||||
|
});
|
||||||
|
|
@ -66,6 +66,7 @@
|
||||||
"@replit/codemirror-lang-solidity": "^6.0.2",
|
"@replit/codemirror-lang-solidity": "^6.0.2",
|
||||||
"@replit/codemirror-lang-svelte": "^6.0.0",
|
"@replit/codemirror-lang-svelte": "^6.0.0",
|
||||||
"@replit/codemirror-vim": "^6.2.1",
|
"@replit/codemirror-vim": "^6.2.1",
|
||||||
|
"@sentry/nextjs": "^9",
|
||||||
"@shopify/lang-jsonc": "^1.0.0",
|
"@shopify/lang-jsonc": "^1.0.0",
|
||||||
"@sourcebot/crypto": "^0.1.0",
|
"@sourcebot/crypto": "^0.1.0",
|
||||||
"@sourcebot/db": "^0.1.0",
|
"@sourcebot/db": "^0.1.0",
|
||||||
|
|
|
||||||
13
packages/web/sentry.client.config.ts
Normal file
13
packages/web/sentry.client.config.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
// This file configures the initialization of Sentry on the client.
|
||||||
|
// The config you add here will be used whenever a users loads a page in their browser.
|
||||||
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||||
|
|
||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: process.env.NEXT_PUBLIC_SENTRY_WEBAPP_DSN,
|
||||||
|
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || 'unknown',
|
||||||
|
|
||||||
|
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||||
|
debug: false,
|
||||||
|
});
|
||||||
14
packages/web/sentry.edge.config.ts
Normal file
14
packages/web/sentry.edge.config.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
|
||||||
|
// The config you add here will be used whenever one of the edge features is loaded.
|
||||||
|
// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
|
||||||
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||||
|
|
||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: process.env.NEXT_PUBLIC_SENTRY_WEBAPP_DSN,
|
||||||
|
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || 'unknown',
|
||||||
|
|
||||||
|
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||||
|
debug: false,
|
||||||
|
});
|
||||||
13
packages/web/sentry.server.config.ts
Normal file
13
packages/web/sentry.server.config.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
// This file configures the initialization of Sentry on the server.
|
||||||
|
// The config you add here will be used whenever the server handles a request.
|
||||||
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||||
|
|
||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: process.env.NEXT_PUBLIC_SENTRY_WEBAPP_DSN,
|
||||||
|
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || 'unknown',
|
||||||
|
|
||||||
|
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||||
|
debug: false,
|
||||||
|
});
|
||||||
23
packages/web/src/app/global-error.tsx
Normal file
23
packages/web/src/app/global-error.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
import NextError from "next/error";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export default function GlobalError({ error }: { error: Error & { digest?: string } }) {
|
||||||
|
useEffect(() => {
|
||||||
|
Sentry.captureException(error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
{/* `NextError` is the default Next.js error page component. Its type
|
||||||
|
definition requires a `statusCode` prop. However, since the App Router
|
||||||
|
does not expose status codes for errors, we simply pass 0 to render a
|
||||||
|
generic error message. */}
|
||||||
|
<NextError statusCode={0} />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
packages/web/src/instrumentation.ts
Normal file
13
packages/web/src/instrumentation.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import * as Sentry from '@sentry/nextjs';
|
||||||
|
|
||||||
|
export async function register() {
|
||||||
|
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||||
|
await import('../sentry.server.config');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NEXT_RUNTIME === 'edge') {
|
||||||
|
await import('../sentry.edge.config');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const onRequestError = Sentry.captureRequestError;
|
||||||
Loading…
Reference in a new issue