Skip to content

Commit f74ec35

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

File tree

4 files changed

+79
-28
lines changed

4 files changed

+79
-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: 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: 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: 73 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 { 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
@@ -55,10 +105,11 @@ export async function detectKeylessEnvDrift(): Promise<void> {
55105
}
56106

57107
// Compare publishable keys
58-
const publicKeyMatch = Boolean(envPublishableKey === keylessFile?.publishableKey);
59-
108+
const publicKeyMatch = Boolean(
109+
envPublishableKey && keylessFile?.publishableKey && envPublishableKey === keylessFile.publishableKey,
110+
);
60111
// Compare secret keys
61-
const secretKeyMatch = Boolean(envSecretKey === keylessFile?.secretKey);
112+
const secretKeyMatch = Boolean(envSecretKey && keylessFile?.secretKey && envSecretKey === keylessFile.secretKey);
62113

63114
// Check if there's a drift (mismatch between env vars and keyless file)
64115
const hasDrift = !publicKeyMatch || !secretKeyMatch;
@@ -69,7 +120,7 @@ export async function detectKeylessEnvDrift(): Promise<void> {
69120
envVarsMissing,
70121
keylessFileHasKeys,
71122
keylessPublishableKey: keylessFile.publishableKey,
72-
envPublishableKey: envPublishableKey as string,
123+
envPublishableKey: envPublishableKey ?? '',
73124
};
74125

75126
// Create a clerk client to access telemetry
@@ -79,14 +130,18 @@ export async function detectKeylessEnvDrift(): Promise<void> {
79130
});
80131

81132
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);
133+
const shouldFireEvent = await tryMarkTelemetryEventAsFired();
134+
135+
if (shouldFireEvent) {
136+
// Fire drift detected event only if we successfully created the flag
137+
const driftDetectedEvent: TelemetryEventRaw<EventKeylessEnvDriftPayload> = {
138+
event: EVENT_KEYLESS_ENV_DRIFT_DETECTED,
139+
eventSamplingRate: EVENT_SAMPLING_RATE,
140+
payload,
141+
};
142+
143+
clerkClient.telemetry?.record(driftDetectedEvent);
144+
}
90145
}
91146
} catch (error) {
92147
// Silently handle errors to avoid breaking the application

0 commit comments

Comments
 (0)