Skip to content

Commit 0c5b8fe

Browse files
Create file to control telemetry event firing
1 parent 3719ab0 commit 0c5b8fe

File tree

4 files changed

+116
-28
lines changed

4 files changed

+116
-28
lines changed

packages/nextjs/src/app-router/keyless-actions.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { redirect, RedirectType } from 'next/navigation';
66
import { errorThrower } from '../server/errorThrower';
77
import { detectClerkMiddleware } from '../server/headers-utils';
88
import { getKeylessCookieName, getKeylessCookieValue } from '../server/keyless';
9-
import { detectKeylessEnvDrift } from '../server/keyless-telemetry';
109
import { canUseKeyless } from '../utils/feature-flags';
1110

1211
type SetCookieOptions = Parameters<Awaited<ReturnType<typeof cookies>>['set']>[2];
@@ -62,9 +61,6 @@ export async function createOrReadKeylessAction(): Promise<null | Omit<Accountle
6261
return null;
6362
}
6463

65-
// Detect environment variable drift and fire telemetry events
66-
await detectKeylessEnvDrift();
67-
6864
const { clerkDevelopmentCache, createKeylessModeMessage } = await import('../server/keyless-log-cache.js');
6965

7066
/**

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

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

Lines changed: 0 additions & 6 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,11 +47,6 @@ export const KeylessProvider = async (props: KeylessProviderProps) => {
4847
.then(mod => mod.createOrReadKeyless())
4948
.catch(() => null);
5049

51-
// Detect environment variable drift and fire telemetry events
52-
if (newOrReadKeys) {
53-
await detectKeylessEnvDrift();
54-
}
55-
5650
const { clerkDevelopmentCache, createConfirmationMessage, createKeylessModeMessage } = await import(
5751
'../../server/keyless-log-cache.js'
5852
);

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

Lines changed: 107 additions & 18 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 { dirname, 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,72 @@ 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 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
2687
*/
2788
export async function detectKeylessEnvDrift(): Promise<void> {
2889
// Only run on server side
@@ -45,31 +106,57 @@ export async function detectKeylessEnvDrift(): Promise<void> {
45106
const envPublishableKey = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY;
46107
const envSecretKey = process.env.CLERK_SECRET_KEY;
47108

48-
// Check if environment variables are missing, and keys exist in keyless file
49-
const envVarsMissing = !envPublishableKey && !envSecretKey;
109+
// Check the state of environment variables and keyless file
110+
const hasEnvVars = Boolean(envPublishableKey || envSecretKey);
50111
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+
}
51119

52120
if (envVarsMissing && keylessFileHasKeys) {
53121
// Environment variables are missing but keyless file has keys - this is normal for keyless mode
54122
return;
55123
}
56124

57-
// Compare publishable keys
58-
const publicKeyMatch = Boolean(envPublishableKey === keylessFile?.publishableKey);
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+
);
59139

60-
// Compare secret keys
61-
const secretKeyMatch = Boolean(envSecretKey === keylessFile?.secretKey);
140+
const secretKeyMatch = Boolean(envSecretKey && keylessFile.secretKey && envSecretKey === keylessFile.secretKey);
62141

63-
// Check if there's a drift (mismatch between env vars and keyless file)
64-
const hasDrift = !publicKeyMatch || !secretKeyMatch;
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+
}
65152

66153
const payload: EventKeylessEnvDriftPayload = {
67154
publicKeyMatch,
68155
secretKeyMatch,
69156
envVarsMissing,
70157
keylessFileHasKeys,
71-
keylessPublishableKey: keylessFile.publishableKey,
72-
envPublishableKey: envPublishableKey as string,
158+
keylessPublishableKey: keylessFile.publishableKey ?? '',
159+
envPublishableKey: envPublishableKey ?? '',
73160
};
74161

75162
// Create a clerk client to access telemetry
@@ -78,8 +165,10 @@ export async function detectKeylessEnvDrift(): Promise<void> {
78165
secretKey: keylessFile.secretKey,
79166
});
80167

81-
if (hasDrift) {
82-
// Fire drift detected event
168+
const shouldFireEvent = await tryMarkTelemetryEventAsFired();
169+
170+
if (shouldFireEvent) {
171+
// Fire drift detected event only if we successfully created the flag
83172
const driftDetectedEvent: TelemetryEventRaw<EventKeylessEnvDriftPayload> = {
84173
event: EVENT_KEYLESS_ENV_DRIFT_DETECTED,
85174
eventSamplingRate: EVENT_SAMPLING_RATE,

0 commit comments

Comments
 (0)