Skip to content

Commit 2f5ebd9

Browse files
feat(analytics): restructure PostHog config under analytics.posthog + wire env vars (#3641)
Move config.posthog.* → config.analytics.posthog.*, rename apiKey → key, and wire DEVKIT_NODE_analytics_posthog_* env vars so Node prod ingestion actually reads POSTHOG_* values from the manifest. Aligns Node convention with Vue (config.analytics.posthog.key) per Wave 3 of PostHog Cloud migration. All consumers and test fixtures updated. Fixes: prod logs "WARN analytics PostHog not configured" caused by Wave 1 leaving env reads commented out in development.config.js.
1 parent 131eddf commit 2f5ebd9

12 files changed

Lines changed: 87 additions & 81 deletions

config/defaults/development.config.js

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,17 @@ const config = {
7979
trust: {
8080
proxy: false,
8181
},
82-
posthog: {
83-
enabled: false, // set to true + apiKey to activate (default off, no breakage on unconfigured projects)
84-
// apiKey: process.env.DEVKIT_NODE_posthog_apiKey ?? '',
85-
// host: process.env.DEVKIT_NODE_posthog_host ?? 'https://eu.i.posthog.com',
86-
// appTag: process.env.DEVKIT_NODE_posthog_appTag ?? '', // e.g. 'trawl', 'comes' — auto-injected on every capture
87-
flushAt: 20,
88-
flushInterval: 10000,
89-
errorTracking: true, // PostHog Error Tracking — active when posthog.apiKey is set
90-
autoCapture: false, // opt-in: auto-capture api_request events (default: off)
82+
analytics: {
83+
posthog: {
84+
enabled: process.env.DEVKIT_NODE_analytics_posthog_enabled === 'true',
85+
key: process.env.DEVKIT_NODE_analytics_posthog_key ?? '',
86+
host: process.env.DEVKIT_NODE_analytics_posthog_host ?? 'https://eu.i.posthog.com',
87+
appTag: process.env.DEVKIT_NODE_analytics_posthog_appTag ?? '',
88+
flushAt: 20,
89+
flushInterval: 10000,
90+
errorTracking: process.env.DEVKIT_NODE_analytics_posthog_errorTracking === 'true',
91+
autoCapture: process.env.DEVKIT_NODE_analytics_posthog_autoCapture === 'true',
92+
},
9193
},
9294
domain: '',
9395
cookie: {

lib/services/analytics.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ import config from '../../config/index.js';
1010
let client = null;
1111

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

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

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

5858
/**
5959
* Capture an analytics event with automatic context injection.
60-
* Auto-injects `app` (from config.posthog.appTag) and `env` (NODE_ENV)
60+
* Auto-injects `app` (from config.analytics.posthog.appTag) and `env` (NODE_ENV)
6161
* into every event. Custom properties take precedence over defaults.
6262
* No-op when client is not initialised, distinctId or event are missing.
6363
*

lib/services/errorTracker.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import analyticsService from './analytics.js';
77
/**
88
* Capture an exception in PostHog.
99
*
10-
* Active when `config.posthog.apiKey` is set AND
11-
* `config.posthog.errorTracking === true`.
10+
* Active when `config.analytics.posthog.key` is set AND
11+
* `config.analytics.posthog.errorTracking === true`.
1212
*
1313
* Safe no-op when PostHog is not configured.
1414
*
@@ -19,8 +19,8 @@ import analyticsService from './analytics.js';
1919
* @returns {void}
2020
*/
2121
const captureException = (err, ctx = {}) => {
22-
const posthogConfig = config?.posthog ?? {};
23-
if (posthogConfig.apiKey && posthogConfig.errorTracking === true) {
22+
const posthogConfig = config?.analytics?.posthog ?? {};
23+
if (posthogConfig.key && posthogConfig.errorTracking === true) {
2424
analyticsService.captureException(err, ctx);
2525
}
2626
};

lib/services/express.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,10 +292,10 @@ const init = async () => {
292292
// Initialize analytics (PostHog).
293293
// Mounted after pre-parser routes so webhooks are not tracked.
294294
// Wrapped in try/catch so analytics failures never prevent app startup.
295-
// auto-capture middleware is opt-in: only mounted when posthog.autoCapture === true.
295+
// auto-capture middleware is opt-in: only mounted when analytics.posthog.autoCapture === true.
296296
try {
297297
await AnalyticsService.init();
298-
if (config.posthog?.autoCapture === true) {
298+
if (config.analytics?.posthog?.autoCapture === true) {
299299
app.use(analyticsMiddleware);
300300
}
301301
} catch (err) {

lib/services/tests/analytics.capture.unit.tests.js

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ describe('Analytics capture() and enabled-flag:', () => {
3535
// 1. enabled=false disables client creation
3636
// ─────────────────────────────────────────────────────────────────
3737
describe('enabled flag:', () => {
38-
test('returns null client when enabled=false even if apiKey is present', async () => {
38+
test('returns null client when enabled=false even if key is present', async () => {
3939
jest.unstable_mockModule('../../../config/index.js', () => ({
40-
default: { posthog: { enabled: false, apiKey: 'phc_test_key', host: 'https://eu.i.posthog.com' } },
40+
default: { analytics: { posthog: { enabled: false, key: 'phc_test_key', host: 'https://eu.i.posthog.com' } } },
4141
}));
4242

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

272272
const mod = await import('../analytics.js');

lib/services/tests/analytics.forRequest.unit.tests.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ describe('analytics request-aware feature-flag helpers:', () => {
3434
}));
3535

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

4040
const mod = await import('../analytics.js');
@@ -145,7 +145,7 @@ describe('analytics request-aware feature-flag helpers:', () => {
145145
await AnalyticsService.shutdown();
146146
jest.resetModules();
147147
jest.unstable_mockModule('../../../config/index.js', () => ({
148-
default: { posthog: {} },
148+
default: { analytics: { posthog: {} } },
149149
}));
150150
const mod = await import('../analytics.js');
151151
const svc = mod.default;
@@ -197,7 +197,7 @@ describe('analytics request-aware feature-flag helpers:', () => {
197197
await AnalyticsService.shutdown();
198198
jest.resetModules();
199199
jest.unstable_mockModule('../../../config/index.js', () => ({
200-
default: { posthog: {} },
200+
default: { analytics: { posthog: {} } },
201201
}));
202202
const mod = await import('../analytics.js');
203203
const svc = mod.default;

lib/services/tests/analytics.service.resilience.unit.tests.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ describe('Analytics service resilience tests:', () => {
2828
}));
2929

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

3434
const mod = await import('../analytics.js');

lib/services/tests/analytics.service.unit.tests.js

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ describe('Analytics service unit tests:', () => {
3232
});
3333

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

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

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

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

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

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

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

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

100100
const mod = await import('../analytics.js');
@@ -109,10 +109,12 @@ describe('Analytics service unit tests:', () => {
109109
beforeEach(async () => {
110110
jest.unstable_mockModule('../../../config/index.js', () => ({
111111
default: {
112-
posthog: {
113-
enabled: true,
114-
apiKey: 'phc_test_key',
115-
host: 'https://test.posthog.com',
112+
analytics: {
113+
posthog: {
114+
enabled: true,
115+
key: 'phc_test_key',
116+
host: 'https://test.posthog.com',
117+
},
116118
},
117119
},
118120
}));
@@ -136,10 +138,12 @@ describe('Analytics service unit tests:', () => {
136138
}));
137139
jest.unstable_mockModule('../../../config/index.js', () => ({
138140
default: {
139-
posthog: {
140-
enabled: true,
141-
apiKey: 'phc_test_key',
142-
host: '',
141+
analytics: {
142+
posthog: {
143+
enabled: true,
144+
key: 'phc_test_key',
145+
host: '',
146+
},
143147
},
144148
},
145149
}));

0 commit comments

Comments
 (0)