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
20 changes: 11 additions & 9 deletions config/defaults/development.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,17 @@ const config = {
trust: {
proxy: false,
},
posthog: {
enabled: false, // set to true + apiKey to activate (default off, no breakage on unconfigured projects)
// apiKey: process.env.DEVKIT_NODE_posthog_apiKey ?? '',
// host: process.env.DEVKIT_NODE_posthog_host ?? 'https://eu.i.posthog.com',
// appTag: process.env.DEVKIT_NODE_posthog_appTag ?? '', // e.g. 'trawl', 'comes' — auto-injected on every capture
flushAt: 20,
flushInterval: 10000,
errorTracking: true, // PostHog Error Tracking — active when posthog.apiKey is set
autoCapture: false, // opt-in: auto-capture api_request events (default: off)
analytics: {
posthog: {
enabled: process.env.DEVKIT_NODE_analytics_posthog_enabled === 'true',
key: process.env.DEVKIT_NODE_analytics_posthog_key ?? '',
host: process.env.DEVKIT_NODE_analytics_posthog_host ?? 'https://eu.i.posthog.com',
appTag: process.env.DEVKIT_NODE_analytics_posthog_appTag ?? '',
flushAt: 20,
flushInterval: 10000,
errorTracking: process.env.DEVKIT_NODE_analytics_posthog_errorTracking === 'true',
autoCapture: process.env.DEVKIT_NODE_analytics_posthog_autoCapture === 'true',
},
},
domain: '',
cookie: {
Expand Down
12 changes: 6 additions & 6 deletions lib/services/analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ import config from '../../config/index.js';
let client = null;

/**
* Resolved at init time from config.posthog.appTag.
* Resolved at init time from config.analytics.posthog.appTag.
* Stored here so capture() doesn't re-read config on every call.
* @type {string|undefined}
*/
let _appTag;

/**
* Initialise the PostHog client using application config.
* When `posthog.enabled` is false OR `posthog.apiKey` is absent the service
* When `analytics.posthog.enabled` is false OR `analytics.posthog.key` is absent the service
* stays in no-op mode — every public method silently returns without
* side-effects so that downstream projects that don't use PostHog are
* never affected.
Expand All @@ -30,13 +30,13 @@ let _appTag;
*/
const init = async () => {
if (client) return; // already initialised — singleton guard
const { enabled, apiKey, host, flushAt, flushInterval, appTag } = config.posthog ?? {};
if (!enabled || !apiKey) return;
const { enabled, key, host, flushAt, flushInterval, appTag } = config.analytics?.posthog ?? {};
if (!enabled || !key) return;
const { PostHog } = await import('posthog-node');
const options = { host: host || 'https://eu.i.posthog.com' };
if (flushAt != null) options.flushAt = flushAt;
if (flushInterval != null) options.flushInterval = flushInterval;
client = new PostHog(apiKey, options);
client = new PostHog(key, options);
_appTag = appTag;
};

Expand All @@ -57,7 +57,7 @@ const track = (distinctId, event, properties, groups) => {

/**
* Capture an analytics event with automatic context injection.
* Auto-injects `app` (from config.posthog.appTag) and `env` (NODE_ENV)
* Auto-injects `app` (from config.analytics.posthog.appTag) and `env` (NODE_ENV)
* into every event. Custom properties take precedence over defaults.
* No-op when client is not initialised, distinctId or event are missing.
*
Expand Down
8 changes: 4 additions & 4 deletions lib/services/errorTracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import analyticsService from './analytics.js';
/**
* Capture an exception in PostHog.
*
* Active when `config.posthog.apiKey` is set AND
* `config.posthog.errorTracking === true`.
* Active when `config.analytics.posthog.key` is set AND
* `config.analytics.posthog.errorTracking === true`.
*
* Safe no-op when PostHog is not configured.
*
Expand All @@ -19,8 +19,8 @@ import analyticsService from './analytics.js';
* @returns {void}
*/
const captureException = (err, ctx = {}) => {
const posthogConfig = config?.posthog ?? {};
if (posthogConfig.apiKey && posthogConfig.errorTracking === true) {
const posthogConfig = config?.analytics?.posthog ?? {};
if (posthogConfig.key && posthogConfig.errorTracking === true) {
analyticsService.captureException(err, ctx);
Comment on lines 8 to 24
}
};
Expand Down
4 changes: 2 additions & 2 deletions lib/services/express.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,10 +292,10 @@ const init = async () => {
// Initialize analytics (PostHog).
// Mounted after pre-parser routes so webhooks are not tracked.
// Wrapped in try/catch so analytics failures never prevent app startup.
// auto-capture middleware is opt-in: only mounted when posthog.autoCapture === true.
// auto-capture middleware is opt-in: only mounted when analytics.posthog.autoCapture === true.
try {
await AnalyticsService.init();
if (config.posthog?.autoCapture === true) {
if (config.analytics?.posthog?.autoCapture === true) {
app.use(analyticsMiddleware);
}
Comment on lines +295 to 300
} catch (err) {
Expand Down
28 changes: 14 additions & 14 deletions lib/services/tests/analytics.capture.unit.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ describe('Analytics capture() and enabled-flag:', () => {
// 1. enabled=false disables client creation
// ─────────────────────────────────────────────────────────────────
describe('enabled flag:', () => {
test('returns null client when enabled=false even if apiKey is present', async () => {
test('returns null client when enabled=false even if key is present', async () => {
jest.unstable_mockModule('../../../config/index.js', () => ({
default: { posthog: { enabled: false, apiKey: 'phc_test_key', host: 'https://eu.i.posthog.com' } },
default: { analytics: { posthog: { enabled: false, key: 'phc_test_key', host: 'https://eu.i.posthog.com' } } },
}));

const mod = await import('../analytics.js');
Expand All @@ -51,9 +51,9 @@ describe('Analytics capture() and enabled-flag:', () => {
expect(mockPostHogInstance.capture).not.toHaveBeenCalled();
});

test('returns null client when apiKey is missing even if enabled=true', async () => {
test('returns null client when key is missing even if enabled=true', async () => {
jest.unstable_mockModule('../../../config/index.js', () => ({
default: { posthog: { enabled: true } },
default: { analytics: { posthog: { enabled: true } } },
}));

const mod = await import('../analytics.js');
Expand All @@ -66,9 +66,9 @@ describe('Analytics capture() and enabled-flag:', () => {
expect(PostHog).not.toHaveBeenCalled();
});

test('creates client when enabled=true and apiKey is present', async () => {
test('creates client when enabled=true and key is present', async () => {
jest.unstable_mockModule('../../../config/index.js', () => ({
default: { posthog: { enabled: true, apiKey: 'phc_test_key', host: 'https://eu.i.posthog.com' } },
default: { analytics: { posthog: { enabled: true, key: 'phc_test_key', host: 'https://eu.i.posthog.com' } } },
}));

const mod = await import('../analytics.js');
Expand All @@ -82,7 +82,7 @@ describe('Analytics capture() and enabled-flag:', () => {

test('passes flushAt and flushInterval to PostHog constructor', async () => {
jest.unstable_mockModule('../../../config/index.js', () => ({
default: { posthog: { enabled: true, apiKey: 'phc_key', host: 'https://eu.i.posthog.com', flushAt: 20, flushInterval: 10000 } },
default: { analytics: { posthog: { enabled: true, key: 'phc_key', host: 'https://eu.i.posthog.com', flushAt: 20, flushInterval: 10000 } } },
}));

const mod = await import('../analytics.js');
Expand All @@ -100,7 +100,7 @@ describe('Analytics capture() and enabled-flag:', () => {

test('singleton: two init() calls on the same module instance result in one PostHog client', async () => {
jest.unstable_mockModule('../../../config/index.js', () => ({
default: { posthog: { enabled: true, apiKey: 'phc_key', host: 'https://eu.i.posthog.com' } },
default: { analytics: { posthog: { enabled: true, key: 'phc_key', host: 'https://eu.i.posthog.com' } } },
}));

const mod = await import('../analytics.js');
Expand All @@ -120,7 +120,7 @@ describe('Analytics capture() and enabled-flag:', () => {
describe('capture() no-ops:', () => {
test('is a no-op when client is null', async () => {
jest.unstable_mockModule('../../../config/index.js', () => ({
default: { posthog: { enabled: false, apiKey: 'phc_key' } },
default: { analytics: { posthog: { enabled: false, key: 'phc_key' } } },
}));

const mod = await import('../analytics.js');
Expand All @@ -134,7 +134,7 @@ describe('Analytics capture() and enabled-flag:', () => {

test('is a no-op when distinctId is missing', async () => {
jest.unstable_mockModule('../../../config/index.js', () => ({
default: { posthog: { enabled: true, apiKey: 'phc_key', host: 'https://eu.i.posthog.com' } },
default: { analytics: { posthog: { enabled: true, key: 'phc_key', host: 'https://eu.i.posthog.com' } } },
}));

const mod = await import('../analytics.js');
Expand All @@ -148,7 +148,7 @@ describe('Analytics capture() and enabled-flag:', () => {

test('is a no-op when event is missing', async () => {
jest.unstable_mockModule('../../../config/index.js', () => ({
default: { posthog: { enabled: true, apiKey: 'phc_key', host: 'https://eu.i.posthog.com' } },
default: { analytics: { posthog: { enabled: true, key: 'phc_key', host: 'https://eu.i.posthog.com' } } },
}));

const mod = await import('../analytics.js');
Expand All @@ -167,7 +167,7 @@ describe('Analytics capture() and enabled-flag:', () => {
describe('capture() property injection:', () => {
beforeEach(async () => {
jest.unstable_mockModule('../../../config/index.js', () => ({
default: { posthog: { enabled: true, apiKey: 'phc_key', host: 'https://eu.i.posthog.com', appTag: 'myapp' } },
default: { analytics: { posthog: { enabled: true, key: 'phc_key', host: 'https://eu.i.posthog.com', appTag: 'myapp' } } },
}));

const mod = await import('../analytics.js');
Expand Down Expand Up @@ -208,7 +208,7 @@ describe('Analytics capture() and enabled-flag:', () => {
}));

jest.unstable_mockModule('../../../config/index.js', () => ({
default: { posthog: { enabled: true, apiKey: 'phc_key', host: 'https://eu.i.posthog.com' } },
default: { analytics: { posthog: { enabled: true, key: 'phc_key', host: 'https://eu.i.posthog.com' } } },
}));

const mod = await import('../analytics.js');
Expand Down Expand Up @@ -266,7 +266,7 @@ describe('Analytics capture() and enabled-flag:', () => {
describe('shutdown idempotency:', () => {
test('two shutdown() calls invoke client.shutdown exactly once', async () => {
jest.unstable_mockModule('../../../config/index.js', () => ({
default: { posthog: { enabled: true, apiKey: 'phc_key', host: 'https://eu.i.posthog.com' } },
default: { analytics: { posthog: { enabled: true, key: 'phc_key', host: 'https://eu.i.posthog.com' } } },
}));

const mod = await import('../analytics.js');
Expand Down
6 changes: 3 additions & 3 deletions lib/services/tests/analytics.forRequest.unit.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('analytics request-aware feature-flag helpers:', () => {
}));

jest.unstable_mockModule('../../../config/index.js', () => ({
default: { posthog: { enabled: true, apiKey: 'phk_test', host: 'https://posthog.test' } },
default: { analytics: { posthog: { enabled: true, key: 'phk_test', host: 'https://posthog.test' } } },
}));

const mod = await import('../analytics.js');
Expand Down Expand Up @@ -145,7 +145,7 @@ describe('analytics request-aware feature-flag helpers:', () => {
await AnalyticsService.shutdown();
jest.resetModules();
jest.unstable_mockModule('../../../config/index.js', () => ({
default: { posthog: {} },
default: { analytics: { posthog: {} } },
}));
const mod = await import('../analytics.js');
const svc = mod.default;
Expand Down Expand Up @@ -197,7 +197,7 @@ describe('analytics request-aware feature-flag helpers:', () => {
await AnalyticsService.shutdown();
jest.resetModules();
jest.unstable_mockModule('../../../config/index.js', () => ({
default: { posthog: {} },
default: { analytics: { posthog: {} } },
}));
const mod = await import('../analytics.js');
const svc = mod.default;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('Analytics service resilience tests:', () => {
}));

jest.unstable_mockModule('../../../config/index.js', () => ({
default: { posthog: { enabled: true, apiKey: 'phc_key', host: 'https://test.posthog.com' } },
default: { analytics: { posthog: { enabled: true, key: 'phc_key', host: 'https://test.posthog.com' } } },
}));

const mod = await import('../analytics.js');
Expand Down
32 changes: 18 additions & 14 deletions lib/services/tests/analytics.service.unit.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ describe('Analytics service unit tests:', () => {
});

describe('no-op mode (PostHog not configured)', () => {
test('should not create a client when apiKey is missing', async () => {
test('should not create a client when key is missing', async () => {
jest.unstable_mockModule('../../../config/index.js', () => ({
default: { posthog: {} },
default: { analytics: { posthog: {} } },
}));

const mod = await import('../analytics.js');
Expand All @@ -49,7 +49,7 @@ describe('Analytics service unit tests:', () => {
expect(PostHog).not.toHaveBeenCalled();
});

test('should not create a client when posthog config section is missing', async () => {
test('should not create a client when analytics.posthog config section is missing', async () => {
jest.unstable_mockModule('../../../config/index.js', () => ({
default: {},
}));
Expand All @@ -66,7 +66,7 @@ describe('Analytics service unit tests:', () => {

test('getFeatureFlag should return undefined when not configured', async () => {
jest.unstable_mockModule('../../../config/index.js', () => ({
default: { posthog: {} },
default: { analytics: { posthog: {} } },
}));

const mod = await import('../analytics.js');
Expand All @@ -80,7 +80,7 @@ describe('Analytics service unit tests:', () => {

test('isFeatureEnabled should return undefined when not configured', async () => {
jest.unstable_mockModule('../../../config/index.js', () => ({
default: { posthog: {} },
default: { analytics: { posthog: {} } },
}));

const mod = await import('../analytics.js');
Expand All @@ -94,7 +94,7 @@ describe('Analytics service unit tests:', () => {

test('shutdown should be safe when not configured', async () => {
jest.unstable_mockModule('../../../config/index.js', () => ({
default: { posthog: {} },
default: { analytics: { posthog: {} } },
}));

const mod = await import('../analytics.js');
Expand All @@ -109,10 +109,12 @@ describe('Analytics service unit tests:', () => {
beforeEach(async () => {
jest.unstable_mockModule('../../../config/index.js', () => ({
default: {
posthog: {
enabled: true,
apiKey: 'phc_test_key',
host: 'https://test.posthog.com',
analytics: {
posthog: {
enabled: true,
key: 'phc_test_key',
host: 'https://test.posthog.com',
},
},
},
}));
Expand All @@ -136,10 +138,12 @@ describe('Analytics service unit tests:', () => {
}));
jest.unstable_mockModule('../../../config/index.js', () => ({
default: {
posthog: {
enabled: true,
apiKey: 'phc_test_key',
host: '',
analytics: {
posthog: {
enabled: true,
key: 'phc_test_key',
host: '',
},
},
},
}));
Expand Down
Loading
Loading