Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/sentry.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)
Expand Down
8 changes: 8 additions & 0 deletions src/app/core/models/config.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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-".
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -29,20 +27,17 @@ describe('initializeApplication', () => {
let googleTagManagerConfigurationMock: { set: ReturnType<typeof vi.fn> };
let environment: EnvironmentModel;
let sentryInitMock: ReturnType<typeof vi.fn>;
let sentryMock: SentryMockType;

function setup(platformId: 'browser' | 'server', environmentOverrides: Partial<EnvironmentModel> = {}) {
configServiceMock = { load: vi.fn().mockResolvedValue(undefined) };
googleTagManagerConfigurationMock = { set: vi.fn() };
sentryMock = SentryMock.simple();

TestBed.configureTestingModule({
providers: [
provideOSFCore(),
MockProvider(PLATFORM_ID, platformId),
{ provide: OSFConfigService, useValue: configServiceMock },
{ provide: GoogleTagManagerConfiguration, useValue: googleTagManagerConfigurationMock },
{ provide: SENTRY_TOKEN, useValue: sentryMock },
],
});

Expand All @@ -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()());

Expand All @@ -65,7 +60,7 @@ describe('initializeApplication', () => {
expect(sentryInitMock).toHaveBeenCalledWith(
expect.objectContaining({
dsn: 'https://dsn.example/123',
environment: 'production',
environment: 'staging',
})
);
});
Expand All @@ -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()());

Expand Down
65 changes: 28 additions & 37 deletions src/app/core/provider/application.initialization.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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 },
Expand All @@ -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());
8 changes: 0 additions & 8 deletions src/app/core/provider/environment.provider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
1 change: 1 addition & 0 deletions src/app/core/services/osf-config.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ describe('OSFConfigService', () => {

const mockConfig: ConfigModel = {
sentryDsn: 'https://sentry.example.com/123',
sentryEnvironment: 'staging',
googleTagManagerId: 'GTM-TEST',
googleFilePickerApiKey: '',
googleFilePickerAppId: 0,
Expand Down
11 changes: 8 additions & 3 deletions src/app/shared/models/environment.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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-".
Expand Down
1 change: 1 addition & 0 deletions src/assets/config/template.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"dataciteTrackerRepoId": null,
"dataciteTrackerAddress": "",
"sentryDsn": "",
"sentryEnvironment": "",
"googleTagManagerId": "",
"googleFilePickerApiKey": "",
"googleFilePickerAppId": 0,
Expand Down
Loading