Skip to content
Draft
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ site/
.act.secrets
.openchallenges.toml

# Generated app config files (from templates via envsubst)
**/config/config.json
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding config.json to .gitignore to follow best practices


# Generated Dockerfiles from centralized container image system
**/Dockerfile.generated

Expand Down
7 changes: 7 additions & 0 deletions apps/agora/app/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
"cwd": "{projectRoot}"
}
},
"create-config-json": {
"executor": "nx:run-commands",
"options": {
"command": "node ../../../tools/create-config-json.js",
"cwd": "{projectRoot}"
}
},
Comment on lines +17 to +23
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds support to generate a config.json with nx run agora-app:create-config-json now that config.json is being gitignored.

"build": {
"executor": "@nx/angular:application",
"outputs": ["{options.outputPath}"],
Expand Down
9 changes: 0 additions & 9 deletions apps/agora/app/src/config/config.json

This file was deleted.

16 changes: 9 additions & 7 deletions apps/agora/app/src/config/config.json.template
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
{
"apiDocsUrl": "${API_DOCS_URL}",
"appVersion": "${APP_VERSION}",
"commitSha": "${COMMIT_SHA}",
"csrApiUrl": "${CSR_API_URL}",
"ssrApiUrl": "${SSR_API_URL}",
"googleTagManagerId": "${GOOGLE_TAG_MANAGER_ID}",
"sentryRelease": "${SENTRY_RELEASE}"
"apiDocsUrl": "${API_DOCS_URL:-http://localhost:8000/api-docs}",
"appVersion": "${APP_VERSION:-local}",
"commitSha": "${COMMIT_SHA:-}",
"csrApiUrl": "${CSR_API_URL:-http://localhost:8000/api/v1}",
"ssrApiUrl": "${SSR_API_URL:-http://agora-api:3333/v1}",
"googleTagManagerId": "${GOOGLE_TAG_MANAGER_ID:-}",
"sentryDSN": "${SENTRY_DSN:-}",
"sentryEnvironment": "${SENTRY_ENVIRONMENT:-localhost}",
"sentryRelease": "${SENTRY_RELEASE:-}"
}
8 changes: 2 additions & 6 deletions apps/agora/app/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,8 @@ import { AppComponent } from './app/app.component';

prefetchConfig<AppConfig>().then((config) => {
initSentry({
dsn: 'https://3cfc84951936511803f5c86d82eb9cad@o4510881207418880.ingest.us.sentry.io/4510897622679552',
hostEnvironmentMap: {
'agora-dev.adknowledgeportal.org': 'dev',
'agora-stage.adknowledgeportal.org': 'stage',
'agora.adknowledgeportal.org': 'prod',
},
dsn: config?.sentryDSN,
environment: config?.sentryEnvironment,
release: config?.sentryRelease,
});

Expand Down
7 changes: 7 additions & 0 deletions apps/model-ad/app/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
"cwd": "{projectRoot}"
}
},
"create-config-json": {
"executor": "nx:run-commands",
"options": {
"command": "node ../../../tools/create-config-json.js",
"cwd": "{projectRoot}"
}
},
"build": {
"executor": "@nx/angular:application",
"outputs": ["{options.outputPath}"],
Expand Down
9 changes: 0 additions & 9 deletions apps/model-ad/app/src/config/config.json

This file was deleted.

16 changes: 9 additions & 7 deletions apps/model-ad/app/src/config/config.json.template
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
{
"apiDocsUrl": "${API_DOCS_URL}",
"appVersion": "${APP_VERSION}",
"commitSha": "${COMMIT_SHA}",
"csrApiUrl": "${CSR_API_URL}",
"googleTagManagerId": "${GOOGLE_TAG_MANAGER_ID}",
"sentryRelease": "${SENTRY_RELEASE}",
"ssrApiUrl": "${SSR_API_URL}"
"apiDocsUrl": "${API_DOCS_URL:-http://localhost:8000/api-docs}",
"appVersion": "${APP_VERSION:-local}",
"commitSha": "${COMMIT_SHA:-}",
"csrApiUrl": "${CSR_API_URL:-http://localhost:8000/api/v1}",
"ssrApiUrl": "${SSR_API_URL:-http://model-ad-api:3333/v1}",
"googleTagManagerId": "${GOOGLE_TAG_MANAGER_ID:-}",
"sentryDSN": "${SENTRY_DSN:-}",
"sentryEnvironment": "${SENTRY_ENVIRONMENT:-localhost}",
"sentryRelease": "${SENTRY_RELEASE:-}"
}
8 changes: 2 additions & 6 deletions apps/model-ad/app/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,8 @@ import { AppComponent } from './app/app.component';

prefetchConfig<AppConfig>().then((config) => {
initSentry({
dsn: 'https://bbf0d51ab53013fe73d95348a6bffe61@o4510881207418880.ingest.us.sentry.io/4510896864559104',
hostEnvironmentMap: {
'dev.modeladexplorer.org': 'dev',
'stage.modeladexplorer.org': 'stage',
'modeladexplorer.org': 'prod',
},
dsn: config?.sentryDSN,
environment: config?.sentryEnvironment,
release: config?.sentryRelease,
});

Expand Down
4 changes: 3 additions & 1 deletion libs/agora/config/src/lib/app.config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { InjectionToken } from '@angular/core';

export interface AppConfig {
apiDocsUrl: string;
appVersion: string;
commitSha: string;
apiDocsUrl: string;
csrApiUrl: string;
ssrApiUrl: string;
googleTagManagerId: string;
isPlatformServer: boolean;
sentryDSN: string;
sentryEnvironment: string;
sentryRelease: string;
}

Expand Down
2 changes: 2 additions & 0 deletions libs/agora/testing/src/lib/mocks/config-mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export const configMock: AppConfig = {
ssrApiUrl: 'http://agora-api:3333/v1',
isPlatformServer: false,
googleTagManagerId: '',
sentryDSN: '',
sentryEnvironment: '',
sentryRelease: '',
};
79 changes: 40 additions & 39 deletions libs/explorers/sentry/src/lib/init-sentry.spec.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,73 @@
import * as Sentry from '@sentry/angular';
import { getSentryEnvironment, initSentry, SentryConfig } from './init-sentry';
import { initSentry, SentryConfig } from './init-sentry';

jest.mock('@sentry/angular', () => ({
init: jest.fn(),
}));

const hostEnvironmentMap: Record<string, string> = {
'app-dev.example.com': 'dev',
'app-stage.example.com': 'stage',
'app.example.com': 'prod',
};

const mockConfig: SentryConfig = {
dsn: 'https://test-dsn@sentry.io/123',
hostEnvironmentMap,
};

describe('getSentryEnvironment', () => {
// The "server" (SSR) path cannot be tested in jsdom since window is always defined,
// and passing undefined explicitly triggers the default parameter which reads window.location.hostname.

it('should return "localhost" for localhost', () => {
expect(getSentryEnvironment(hostEnvironmentMap, 'localhost')).toBe('localhost');
describe('initSentry', () => {
afterEach(() => {
(Sentry.init as jest.Mock).mockClear();
});

it('should return "localhost" for 127.0.0.1', () => {
expect(getSentryEnvironment(hostEnvironmentMap, '127.0.0.1')).toBe('localhost');
});
it('should initialize Sentry with environment and release', () => {
initSentry({ ...mockConfig, environment: 'prod', release: '1.0.0+abc1234' });

it('should resolve a mapped hostname to its environment', () => {
expect(getSentryEnvironment(hostEnvironmentMap, 'app-dev.example.com')).toBe('dev');
expect(getSentryEnvironment(hostEnvironmentMap, 'app-stage.example.com')).toBe('stage');
expect(getSentryEnvironment(hostEnvironmentMap, 'app.example.com')).toBe('prod');
expect(Sentry.init).toHaveBeenCalledWith({
dsn: mockConfig.dsn,
environment: 'prod',
release: '1.0.0+abc1234',
sendDefaultPii: false,
});
});

it('should fall back to the raw hostname when not in the map', () => {
expect(getSentryEnvironment(hostEnvironmentMap, 'unknown-host.example.com')).toBe(
'unknown-host.example.com',
);
});
});
it('should fallback to window.location.hostname when environment not provided', () => {
initSentry(mockConfig);

describe('initSentry', () => {
afterEach(() => {
(Sentry.init as jest.Mock).mockClear();
expect(Sentry.init).toHaveBeenCalledWith(
expect.objectContaining({ environment: window.location.hostname }),
);
});

it('should initialize Sentry with release from config', () => {
initSentry({ ...mockConfig, release: '1.0.0+abc1234' });
it('should fallback to window.location.hostname when environment is empty string', () => {
initSentry({ ...mockConfig, environment: '' });

expect(Sentry.init).toHaveBeenCalledWith(
expect.objectContaining({
dsn: mockConfig.dsn,
release: '1.0.0+abc1234',
sendDefaultPii: false,
}),
expect.objectContaining({ environment: window.location.hostname }),
);
});

it('should initialize Sentry without release when release is not provided', () => {
it('should initialize Sentry without release when not provided', () => {
initSentry(mockConfig);

expect(Sentry.init).toHaveBeenCalledWith(expect.objectContaining({ release: undefined }));
});

it('should initialize Sentry without release when release is empty string', () => {
it('should initialize Sentry without release when empty string', () => {
initSentry({ ...mockConfig, release: '' });

expect(Sentry.init).toHaveBeenCalledWith(expect.objectContaining({ release: undefined }));
});

it('should warn and not initialize Sentry when dsn is not provided', () => {
const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
initSentry({});

expect(console.warn).toHaveBeenCalledWith('Sentry DSN is not configured.');
expect(Sentry.init).not.toHaveBeenCalled();
warnSpy.mockRestore();
});

it('should warn and not initialize Sentry when dsn is empty string', () => {
const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
initSentry({ dsn: '' });

expect(console.warn).toHaveBeenCalledWith('Sentry DSN is not configured.');
expect(Sentry.init).not.toHaveBeenCalled();
warnSpy.mockRestore();
});
});
20 changes: 7 additions & 13 deletions libs/explorers/sentry/src/lib/init-sentry.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,21 @@
import * as Sentry from '@sentry/angular';

export interface SentryConfig {
dsn: string;
hostEnvironmentMap: Record<string, string>;
dsn?: string;
environment?: string;
release?: string;
}

export function getSentryEnvironment(
hostEnvironmentMap: Record<string, string>,
hostname = typeof window !== 'undefined' ? window.location.hostname : undefined,
): string {
if (!hostname) return 'server';
if (hostname === 'localhost' || hostname === '127.0.0.1') return 'localhost';

return hostEnvironmentMap[hostname] ?? hostname;
}

export function initSentry(config: SentryConfig): void {
if (typeof window === 'undefined') return;
if (!config.dsn) {
console.warn('Sentry DSN is not configured.');
return;
}

Sentry.init({
dsn: config.dsn,
environment: getSentryEnvironment(config.hostEnvironmentMap),
environment: config.environment || window.location.hostname,
release: config.release || undefined,
sendDefaultPii: false,
});
Expand Down
2 changes: 2 additions & 0 deletions libs/model-ad/config/src/lib/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export interface AppConfig {
ssrApiUrl: string;
googleTagManagerId: string;
isPlatformServer: boolean;
sentryDSN: string;
sentryEnvironment: string;
sentryRelease: string;
}

Expand Down
2 changes: 2 additions & 0 deletions libs/model-ad/testing/src/lib/mocks/config-mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export const configMock: AppConfig = {
googleTagManagerId: '',
ssrApiUrl: 'http://model-ad-api:3333/v1',
isPlatformServer: false,
sentryDSN: '',
sentryEnvironment: '',
sentryRelease: '',
};
32 changes: 32 additions & 0 deletions tools/create-config-json.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env node
/**
* Generates config.json from config.json.template by substituting environment variables.
*
* Supports POSIX shell parameter expansion syntax: ${VAR:-default}
* - If VAR is set, uses its value
* - If VAR is unset or empty, uses the default value
*
* Usage: node tools/create-config-json.js
* (Run from project root, e.g., apps/agora/app)
*/

const fs = require('fs');
const path = require('path');

const templatePath = path.join(process.cwd(), 'src/config/config.json.template');
const outputPath = path.join(process.cwd(), 'src/config/config.json');

if (!fs.existsSync(templatePath)) {
console.error(`Template not found: ${templatePath}`);
process.exit(1);
}

const template = fs.readFileSync(templatePath, 'utf8');

const result = template.replace(/\${([^}]+)}/g, (_, expr) => {
const [varName, defaultVal] = expr.split(':-');
return process.env[varName] || defaultVal || '';
});

fs.writeFileSync(outputPath, result);
console.log(`Created ${outputPath}`);
Loading