Skip to content

Commit 21c2956

Browse files
feat(nextjs) Environment Drift Telemetry Event for Keyless Applications (#6522)
1 parent 173837c commit 21c2956

File tree

3 files changed

+200
-0
lines changed

3 files changed

+200
-0
lines changed

.changeset/bright-parks-search.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@clerk/nextjs': patch
3+
---
4+
5+
Add new telemetry event KEYLESS_ENV_DRIFT_DETECTED to detect drift between publishable and secret keys in keyless apps and values in the .env file.
6+
7+
This event only fires once as controlled with the .clerk/.tmp/telemetry.json file to prevent telemetry event noise

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

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

7070
let output: ReactNode;
7171

72+
try {
73+
const detectKeylessEnvDrift = await import('../../server/keyless-telemetry.js').then(
74+
mod => mod.detectKeylessEnvDrift,
75+
);
76+
await detectKeylessEnvDrift();
77+
} catch {
78+
// ignore
79+
}
80+
7281
if (shouldRunAsKeyless) {
7382
output = (
7483
<KeylessProvider
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import type { TelemetryEventRaw } from '@clerk/types';
2+
import { promises as fs } from 'fs';
3+
import { dirname, join } from 'path';
4+
5+
import { createClerkClientWithOptions } from './createClerkClient';
6+
7+
const EVENT_KEYLESS_ENV_DRIFT_DETECTED = 'KEYLESS_ENV_DRIFT_DETECTED';
8+
const EVENT_SAMPLING_RATE = 1; // 100% sampling rate
9+
const TELEMETRY_FLAG_FILE = '.clerk/.tmp/telemetry.json';
10+
11+
type EventKeylessEnvDriftPayload = {
12+
publicKeyMatch: boolean;
13+
secretKeyMatch: boolean;
14+
envVarsMissing: boolean;
15+
keylessFileHasKeys: boolean;
16+
keylessPublishableKey: string;
17+
envPublishableKey: string;
18+
};
19+
20+
/**
21+
* Gets the absolute path to the telemetry flag file.
22+
*
23+
* This file is used to track whether telemetry events have already been fired
24+
* to prevent duplicate event reporting during the application lifecycle.
25+
*
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 flagFilePath = getTelemetryFlagFilePath();
46+
const flagDirectory = dirname(flagFilePath);
47+
48+
// Ensure the directory exists before attempting to write the file
49+
await fs.mkdir(flagDirectory, { recursive: true });
50+
51+
const flagData = {
52+
firedAt: new Date().toISOString(),
53+
event: EVENT_KEYLESS_ENV_DRIFT_DETECTED,
54+
};
55+
await fs.writeFile(flagFilePath, JSON.stringify(flagData, null, 2), { flag: 'wx' });
56+
return true;
57+
} catch (error: unknown) {
58+
if ((error as { code?: string })?.code === 'EEXIST') {
59+
return false;
60+
}
61+
console.warn('Failed to create telemetry flag file:', error);
62+
return false;
63+
}
64+
}
65+
66+
/**
67+
* Detects and reports environment drift between keyless configuration and environment variables.
68+
*
69+
* This function compares the Clerk keys stored in the keyless configuration file (.clerk/clerk.json)
70+
* with the keys set in environment variables (NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY).
71+
* It only reports drift when there's an actual mismatch between existing keys, not when keys are simply missing.
72+
*
73+
* The function handles several scenarios and only reports drift in specific cases:
74+
* - **Normal keyless mode**: env vars missing but keyless file has keys → no drift (expected)
75+
* - **No configuration**: neither env vars nor keyless file have keys → no drift (nothing to compare)
76+
* - **Actual drift**: env vars exist and don't match keyless file keys → drift detected
77+
* - **Empty keyless file**: keyless file exists but has no keys → no drift (nothing to compare)
78+
*
79+
* Drift is only detected when:
80+
* 1. Both environment variables and keyless file contain keys
81+
* 2. The keys in environment variables don't match the keys in the keyless file
82+
*
83+
* Telemetry events are only fired once per application lifecycle using a flag file mechanism
84+
* to prevent duplicate reporting.
85+
*
86+
* @returns Promise<void> - Function completes silently, errors are logged but don't throw
87+
*/
88+
export async function detectKeylessEnvDrift(): Promise<void> {
89+
// Only run on server side
90+
if (typeof window !== 'undefined') {
91+
return;
92+
}
93+
94+
try {
95+
// Dynamically import server-side dependencies to avoid client-side issues
96+
const { safeParseClerkFile } = await import('./keyless-node.js');
97+
98+
// Read the keyless configuration file
99+
const keylessFile = safeParseClerkFile();
100+
101+
if (!keylessFile) {
102+
return;
103+
}
104+
105+
// Get environment variables
106+
const envPublishableKey = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY;
107+
const envSecretKey = process.env.CLERK_SECRET_KEY;
108+
109+
// Check the state of environment variables and keyless file
110+
const hasEnvVars = Boolean(envPublishableKey || envSecretKey);
111+
const keylessFileHasKeys = Boolean(keylessFile?.publishableKey && keylessFile?.secretKey);
112+
const envVarsMissing = !envPublishableKey && !envSecretKey;
113+
114+
// Early return conditions - no drift to detect in these scenarios:
115+
if (!hasEnvVars && !keylessFileHasKeys) {
116+
// Neither env vars nor keyless file have keys - nothing to compare
117+
return;
118+
}
119+
120+
if (envVarsMissing && keylessFileHasKeys) {
121+
// Environment variables are missing but keyless file has keys - this is normal for keyless mode
122+
return;
123+
}
124+
125+
if (!keylessFileHasKeys) {
126+
// Keyless file doesn't have keys, so no drift can be detected
127+
return;
128+
}
129+
130+
// Only proceed with drift detection if we have something meaningful to compare
131+
if (!hasEnvVars) {
132+
return;
133+
}
134+
135+
// Compare keys only when both sides have values to compare
136+
const publicKeyMatch = Boolean(
137+
envPublishableKey && keylessFile.publishableKey && envPublishableKey === keylessFile.publishableKey,
138+
);
139+
140+
const secretKeyMatch = Boolean(envSecretKey && keylessFile.secretKey && envSecretKey === keylessFile.secretKey);
141+
142+
// Determine if there's an actual drift:
143+
// Drift occurs when we have env vars that don't match the keyless file keys
144+
const hasActualDrift =
145+
(envPublishableKey && keylessFile.publishableKey && !publicKeyMatch) ||
146+
(envSecretKey && keylessFile.secretKey && !secretKeyMatch);
147+
148+
// Only fire telemetry if there's an actual drift (not just missing keys)
149+
if (!hasActualDrift) {
150+
return;
151+
}
152+
153+
const payload: EventKeylessEnvDriftPayload = {
154+
publicKeyMatch,
155+
secretKeyMatch,
156+
envVarsMissing,
157+
keylessFileHasKeys,
158+
keylessPublishableKey: keylessFile.publishableKey ?? '',
159+
envPublishableKey: envPublishableKey ?? '',
160+
};
161+
162+
// Create a clerk client to access telemetry
163+
const clerkClient = createClerkClientWithOptions({
164+
publishableKey: keylessFile.publishableKey,
165+
secretKey: keylessFile.secretKey,
166+
});
167+
168+
const shouldFireEvent = await tryMarkTelemetryEventAsFired();
169+
170+
if (shouldFireEvent) {
171+
// Fire drift detected event only if we successfully created the flag
172+
const driftDetectedEvent: TelemetryEventRaw<EventKeylessEnvDriftPayload> = {
173+
event: EVENT_KEYLESS_ENV_DRIFT_DETECTED,
174+
eventSamplingRate: EVENT_SAMPLING_RATE,
175+
payload,
176+
};
177+
178+
clerkClient.telemetry?.record(driftDetectedEvent);
179+
}
180+
} catch (error) {
181+
// Silently handle errors to avoid breaking the application
182+
console.warn('Failed to detect keyless environment drift:', error);
183+
}
184+
}

0 commit comments

Comments
 (0)