diff --git a/docs/sentry.md b/docs/sentry.md index 27a1ae607..33982b849 100644 --- a/docs/sentry.md +++ b/docs/sentry.md @@ -2,6 +2,17 @@ Sentry collects JavaScript errors from the OSF Angular app in the browser. Many of those events are not application bugs: flaky networks, cancelled requests, missing/deleted API resources, browser extensions, and stale tabs after a deploy. +## Setup + +`Sentry.init` runs in the browser from `src/app/core/provider/application.initialization.provider.ts` after `assets/config/config.json` loads. Set: + +| Key | Role | +| ------------------- | ------------------------------------------------------------------------------------------------ | +| `sentryDsn` | Enables Sentry when non-empty | +| `sentryEnvironment` | Passed as Sentry’s `environment` option (use the same name as other services for managed Sentry) | + +## Filtering + Filtering happens on the client when Sentry starts. Change the lists in `src/app/core/helpers/sentry-filter.helper.ts`. That file is passed into `Sentry.init` from `src/app/core/provider/application.initialization.provider.ts`. [Sentry filtering docs](https://docs.sentry.io/platforms/javascript/configuration/filtering/) diff --git a/src/app/core/models/config.model.ts b/src/app/core/models/config.model.ts index de810b2ef..0b0290457 100644 --- a/src/app/core/models/config.model.ts +++ b/src/app/core/models/config.model.ts @@ -16,6 +16,14 @@ export interface ConfigModel { */ sentryDsn: string; + /** + * Sentry environment name sent with captured events. + * Names are case-sensitive and cannot contain spaces, newlines, or forward slashes. + * + * @example "staging" + */ + sentryEnvironment: string; + /** * The Google Tag Manager ID used to embed GTM scripts for analytics tracking. * This ID typically starts with "GTM-". diff --git a/src/app/core/provider/application.initialization.provider.spec.ts b/src/app/core/provider/application.initialization.provider.spec.ts index 05899ea1d..9e6f41cbc 100644 --- a/src/app/core/provider/application.initialization.provider.spec.ts +++ b/src/app/core/provider/application.initialization.provider.spec.ts @@ -7,11 +7,9 @@ import { OSFConfigService } from '@core/services/osf-config.service'; import { EnvironmentModel } from '@osf/shared/models/environment.model'; import { provideOSFCore } from '@testing/osf.testing.provider'; -import { SentryMock, SentryMockType } from '@testing/providers/sentry-provider.mock'; import { initializeApplication } from './application.initialization.provider'; import { ENVIRONMENT } from './environment.provider'; -import { SENTRY_TOKEN } from './sentry.provider'; import * as Sentry from '@sentry/angular'; import { GoogleTagManagerConfiguration } from 'angular-google-tag-manager'; @@ -29,12 +27,10 @@ describe('initializeApplication', () => { let googleTagManagerConfigurationMock: { set: ReturnType }; let environment: EnvironmentModel; let sentryInitMock: ReturnType; - let sentryMock: SentryMockType; function setup(platformId: 'browser' | 'server', environmentOverrides: Partial = {}) { configServiceMock = { load: vi.fn().mockResolvedValue(undefined) }; googleTagManagerConfigurationMock = { set: vi.fn() }; - sentryMock = SentryMock.simple(); TestBed.configureTestingModule({ providers: [ @@ -42,7 +38,6 @@ describe('initializeApplication', () => { MockProvider(PLATFORM_ID, platformId), { provide: OSFConfigService, useValue: configServiceMock }, { provide: GoogleTagManagerConfiguration, useValue: googleTagManagerConfigurationMock }, - { provide: SENTRY_TOKEN, useValue: sentryMock }, ], }); @@ -56,7 +51,7 @@ describe('initializeApplication', () => { setup('browser', { googleTagManagerId: 'GTM-TEST', sentryDsn: 'https://dsn.example/123', - production: true, + sentryEnvironment: 'staging', }); await TestBed.runInInjectionContext(async () => initializeApplication()()); @@ -65,7 +60,7 @@ describe('initializeApplication', () => { expect(sentryInitMock).toHaveBeenCalledWith( expect.objectContaining({ dsn: 'https://dsn.example/123', - environment: 'production', + environment: 'staging', }) ); }); @@ -84,7 +79,7 @@ describe('initializeApplication', () => { setup('server', { googleTagManagerId: 'GTM-TEST', sentryDsn: 'https://dsn.example/123', - production: true, + sentryEnvironment: 'staging', }); await TestBed.runInInjectionContext(async () => initializeApplication()()); diff --git a/src/app/core/provider/application.initialization.provider.ts b/src/app/core/provider/application.initialization.provider.ts index ed08425c7..d12ffe39a 100644 --- a/src/app/core/provider/application.initialization.provider.ts +++ b/src/app/core/provider/application.initialization.provider.ts @@ -10,13 +10,13 @@ import * as Sentry from '@sentry/angular'; import { GoogleTagManagerConfiguration } from 'angular-google-tag-manager'; /** - * Asynchronous initializer function that loads the Sentry DSN from the config service - * and initializes Sentry at application bootstrap. + * App initializer factory used with `provideAppInitializer`. * - * This function is meant to be used with `provideAppInitializer`, which blocks Angular - * bootstrap until the Promise resolves. This avoids race conditions when reading config. + * Loads runtime config via `OSFConfigService`, then on the browser configures + * Google Tag Manager, Sentry, and New Relic when their config values are present. + * Bootstrap waits until this Promise resolves so config is available before startup. * - * @returns A Promise that resolves once Sentry is initialized (or skipped if no DSN) + * @returns An async initializer that resolves after config load and optional browser setup */ export function initializeApplication() { return async () => { @@ -27,35 +27,30 @@ export function initializeApplication() { await configService.load(); - if (isPlatformBrowser(platformId)) { - const googleTagManagerId = environment.googleTagManagerId; - - if (googleTagManagerId) { - googleTagManagerConfiguration.set({ id: googleTagManagerId }); - } + if (!isPlatformBrowser(platformId)) { + return; + } - const dsn = environment.sentryDsn; + if (environment.googleTagManagerId) { + googleTagManagerConfiguration.set({ id: environment.googleTagManagerId }); + } - if (dsn) { - // More Options - // https://docs.sentry.io/platforms/javascript/guides/angular/configuration/options/ - Sentry.init({ - dsn, - environment: environment.production ? 'production' : 'development', - maxBreadcrumbs: 50, - sampleRate: 1.0, - ignoreErrors: SENTRY_IGNORE_ERRORS, - denyUrls: SENTRY_DENY_URLS, - beforeSend: sentryBeforeSend, - }); - } + if (environment.sentryDsn) { + Sentry.init({ + dsn: environment.sentryDsn, + environment: environment.sentryEnvironment, + maxBreadcrumbs: 50, + sampleRate: 1.0, + ignoreErrors: SENTRY_IGNORE_ERRORS, + denyUrls: SENTRY_DENY_URLS, + beforeSend: sentryBeforeSend, + }); } - if (environment.newRelicEnabled && isPlatformBrowser(platformId)) { + if (environment.newRelicEnabled) { const { BrowserAgent } = await import('@newrelic/browser-agent/loaders/browser-agent'); - const newRelicConfig = { - enabled: environment.newRelicEnabled, + new BrowserAgent({ init: { distributed_tracing: { enabled: environment.newRelicInitDistributedTracingEnabled }, performance: { capture_measures: environment.newRelicInitPerformanceCaptureMeasures }, @@ -76,22 +71,18 @@ export function initializeApplication() { licenseKey: environment.newRelicLoaderConfigLicenseKey, applicationID: environment.newRelicLoaderConfigApplicationID, }, - }; - new BrowserAgent(newRelicConfig); + }); } }; } /** - * Provides the Sentry initialization logic during Angular's application bootstrap phase. - * - * This uses `provideAppInitializer` to block application startup until Sentry is initialized. - * It ensures that the Sentry DSN (fetched from OSFConfigService) is available and configured - * before any errors are handled or reported by the app. + * Registers application bootstrap initialization via `provideAppInitializer`. * - * `initializeSentry` is a function that returns a Promise which resolves after Sentry is fully initialized. + * Blocks startup until runtime config is loaded and browser integrations + * (GTM, Sentry, New Relic) are configured when enabled. * * @see https://docs.sentry.io/platforms/javascript/guides/angular/ - * @see Angular's `provideAppInitializer`: https://angular.io/api/core/provideAppInitializer + * @see https://angular.io/api/core/provideAppInitializer */ export const APPLICATION_INITIALIZATION_PROVIDER = provideAppInitializer(initializeApplication()); diff --git a/src/app/core/provider/environment.provider.spec.ts b/src/app/core/provider/environment.provider.spec.ts index c51c140dd..80978a9f5 100644 --- a/src/app/core/provider/environment.provider.spec.ts +++ b/src/app/core/provider/environment.provider.spec.ts @@ -37,14 +37,6 @@ describe('Provider: Environment', () => { expect(environment.dataciteTrackerRepoId).toBeNull(); }); - it('should allow updating an optional nested object', () => { - environment.activityLogs = { pageSize: 100 }; - expect(environment.activityLogs).toEqual({ pageSize: 100 }); - environment.activityLogs.pageSize = 200; - expect(environment.activityLogs).toEqual({ pageSize: 200 }); - expect(environment.activityLogs.pageSize).toBe(200); - }); - it('should reflect changes in subsequent reads', () => { environment.facebookAppId = 'abc123'; expect(environment.facebookAppId).toBe('abc123'); diff --git a/src/app/core/services/osf-config.service.spec.ts b/src/app/core/services/osf-config.service.spec.ts index 2ad6d0587..7a31b61e1 100644 --- a/src/app/core/services/osf-config.service.spec.ts +++ b/src/app/core/services/osf-config.service.spec.ts @@ -19,6 +19,7 @@ describe('OSFConfigService', () => { const mockConfig: ConfigModel = { sentryDsn: 'https://sentry.example.com/123', + sentryEnvironment: 'staging', googleTagManagerId: 'GTM-TEST', googleFilePickerApiKey: '', googleFilePickerAppId: 0, diff --git a/src/app/shared/models/environment.model.ts b/src/app/shared/models/environment.model.ts index fcc33126d..361902fe5 100644 --- a/src/app/shared/models/environment.model.ts +++ b/src/app/shared/models/environment.model.ts @@ -29,9 +29,6 @@ export interface EnvironmentModel { newRelicLoaderConfigAgentID: string; newRelicLoaderConfigLicenseKey: string; newRelicLoaderConfigApplicationID: string; - activityLogs?: { - pageSize?: number; - }; /** * The DSN (Data Source Name) used to configure Sentry for error tracking. @@ -41,6 +38,14 @@ export interface EnvironmentModel { */ sentryDsn: string; + /** + * Sentry environment name sent with captured events. + * Names are case-sensitive and cannot contain spaces, newlines, or forward slashes. + * + * @example "staging" + */ + sentryEnvironment: string; + /** * The Google Tag Manager ID used to embed GTM scripts for analytics tracking. * This ID typically starts with "GTM-". diff --git a/src/assets/config/template.json b/src/assets/config/template.json index f74a71179..620aa70e5 100644 --- a/src/assets/config/template.json +++ b/src/assets/config/template.json @@ -10,6 +10,7 @@ "dataciteTrackerRepoId": null, "dataciteTrackerAddress": "", "sentryDsn": "", + "sentryEnvironment": "", "googleTagManagerId": "", "googleFilePickerApiKey": "", "googleFilePickerAppId": 0,