Skip to content

Commit 83f4835

Browse files
Create file to control telemetry event firing
1 parent 9897c43 commit 83f4835

File tree

3 files changed

+78
-16
lines changed

3 files changed

+78
-16
lines changed

packages/nextjs/src/app-router/server/ClerkProvider.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ export async function ClerkProvider(
6969

7070
let output: ReactNode;
7171

72+
const detectKeylessEnvDrift = await import('../../server/keyless-telemetry.js').then(
73+
mod => mod.detectKeylessEnvDrift,
74+
);
75+
76+
await detectKeylessEnvDrift();
77+
7278
if (shouldRunAsKeyless) {
7379
output = (
7480
<KeylessProvider

packages/nextjs/src/app-router/server/keyless-provider.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import React from 'react';
66

77
import { createClerkClientWithOptions } from '../../server/createClerkClient';
88
import { collectKeylessMetadata, formatMetadataHeaders } from '../../server/keyless-custom-headers';
9-
import { detectKeylessEnvDrift } from '../../server/keyless-telemetry';
109
import type { NextClerkProviderProps } from '../../types';
1110
import { canUseKeyless } from '../../utils/feature-flags';
1211
import { mergeNextClerkPropsWithEnv } from '../../utils/mergeNextClerkPropsWithEnv';
@@ -48,7 +47,10 @@ export const KeylessProvider = async (props: KeylessProviderProps) => {
4847
.then(mod => mod.createOrReadKeyless())
4948
.catch(() => null);
5049

51-
// Detect environment variable drift and fire telemetry events
50+
const detectKeylessEnvDrift = await import('../../server/keyless-telemetry.js').then(
51+
mod => mod.detectKeylessEnvDrift,
52+
);
53+
5254
if (newOrReadKeys) {
5355
await detectKeylessEnvDrift();
5456
}

packages/nextjs/src/server/keyless-telemetry.ts

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import type { TelemetryEventRaw } from '@clerk/types';
2+
import { promises as fs } from 'fs';
3+
import { join } from 'path';
24

35
import { createClerkClientWithOptions } from './createClerkClient';
46

57
const EVENT_KEYLESS_ENV_DRIFT_DETECTED = 'KEYLESS_ENV_DRIFT_DETECTED';
68
const EVENT_SAMPLING_RATE = 1; // 100% sampling rate
9+
const TELEMETRY_FLAG_FILE = '.clerk/.tmp/telemetry.json';
710

811
type EventKeylessEnvDriftPayload = {
912
publicKeyMatch: boolean;
@@ -15,14 +18,61 @@ type EventKeylessEnvDriftPayload = {
1518
};
1619

1720
/**
18-
* Detects environment variable drift for keyless Next.js applications and fires telemetry events.
21+
* Gets the absolute path to the telemetry flag file.
1922
*
20-
* This function compares the publishableKey and secretKey values from `.clerk/.tmp/keyless.json`
21-
* with the `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` environment variables.
23+
* This file is used to track whether telemetry events have already been fired
24+
* to prevent duplicate event reporting during the application lifecycle.
2225
*
23-
* If there's a mismatch, it fires a `KEYLESS_ENV_DRIFT_DETECTED` event.
24-
* For local testing purposes, it also fires a `KEYLESS_ENV_DRIFT_NOT_DETECTED` event when
25-
* keys exist and match the environment variables.
26+
* @returns The absolute path to the telemetry flag file in the project's .clerk/.tmp directory
27+
*/
28+
function getTelemetryFlagFilePath(): string {
29+
return join(process.cwd(), TELEMETRY_FLAG_FILE);
30+
}
31+
32+
/**
33+
* Attempts to create a telemetry flag file to mark that a telemetry event has been fired.
34+
*
35+
* This function uses the 'wx' flag to create the file atomically - it will only succeed
36+
* if the file doesn't already exist. This ensures that telemetry events are only fired
37+
* once per application lifecycle, preventing duplicate event reporting.
38+
*
39+
* @returns Promise<boolean> - Returns true if the flag file was successfully created (meaning
40+
* the event should be fired), false if the file already exists (meaning the event was
41+
* already fired) or if there was an error creating the file
42+
*/
43+
async function tryMarkTelemetryEventAsFired(): Promise<boolean> {
44+
try {
45+
const flagData = {
46+
firedAt: new Date().toISOString(),
47+
event: EVENT_KEYLESS_ENV_DRIFT_DETECTED,
48+
};
49+
await fs.writeFile(getTelemetryFlagFilePath(), JSON.stringify(flagData, null, 2), { flag: 'wx' });
50+
return true;
51+
} catch (error: unknown) {
52+
if ((error as { code?: string })?.code === 'EEXIST') {
53+
return false;
54+
}
55+
console.warn('Failed to create telemetry flag file:', error);
56+
return false;
57+
}
58+
}
59+
60+
/**
61+
* Detects and reports environment drift between keyless configuration and environment variables.
62+
*
63+
* This function compares the Clerk keys stored in the keyless configuration file (.clerk/clerk.json)
64+
* with the keys set in environment variables (NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY).
65+
* If there's a mismatch (drift), it reports this as a telemetry event to help diagnose configuration issues.
66+
*
67+
* The function handles several scenarios:
68+
* - Normal keyless mode: env vars missing but keyless file has keys (no drift)
69+
* - Drift detected: env vars and keyless file have different keys
70+
* - Mixed configuration: some keys match, others don't
71+
*
72+
* Telemetry events are only fired once per application lifecycle using a flag file mechanism
73+
* to prevent duplicate reporting.
74+
*
75+
* @returns Promise<void> - Function completes silently, errors are logged but don't throw
2676
*/
2777
export async function detectKeylessEnvDrift(): Promise<void> {
2878
// Only run on server side
@@ -79,14 +129,18 @@ export async function detectKeylessEnvDrift(): Promise<void> {
79129
});
80130

81131
if (hasDrift) {
82-
// Fire drift detected event
83-
const driftDetectedEvent: TelemetryEventRaw<EventKeylessEnvDriftPayload> = {
84-
event: EVENT_KEYLESS_ENV_DRIFT_DETECTED,
85-
eventSamplingRate: EVENT_SAMPLING_RATE,
86-
payload,
87-
};
88-
89-
clerkClient.telemetry?.record(driftDetectedEvent);
132+
const shouldFireEvent = await tryMarkTelemetryEventAsFired();
133+
134+
if (shouldFireEvent) {
135+
// Fire drift detected event only if we successfully created the flag
136+
const driftDetectedEvent: TelemetryEventRaw<EventKeylessEnvDriftPayload> = {
137+
event: EVENT_KEYLESS_ENV_DRIFT_DETECTED,
138+
eventSamplingRate: EVENT_SAMPLING_RATE,
139+
payload,
140+
};
141+
142+
clerkClient.telemetry?.record(driftDetectedEvent);
143+
}
90144
}
91145
} catch (error) {
92146
// Silently handle errors to avoid breaking the application

0 commit comments

Comments
 (0)