-
Notifications
You must be signed in to change notification settings - Fork 371
feat(nextjs): Environment drift telemetry event for keyless applications #6522
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
'@clerk/nextjs': patch | ||
--- | ||
|
||
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. | ||
|
||
This event only fires once as controlled with the .clerk/.tmp/telemetry.json file to prevent telemetry event noise |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
import type { TelemetryEventRaw } from '@clerk/types'; | ||
import { promises as fs } from 'fs'; | ||
import { dirname, join } from 'path'; | ||
|
||
import { createClerkClientWithOptions } from './createClerkClient'; | ||
|
||
const EVENT_KEYLESS_ENV_DRIFT_DETECTED = 'KEYLESS_ENV_DRIFT_DETECTED'; | ||
const EVENT_SAMPLING_RATE = 1; // 100% sampling rate | ||
const TELEMETRY_FLAG_FILE = '.clerk/.tmp/telemetry.json'; | ||
|
||
type EventKeylessEnvDriftPayload = { | ||
publicKeyMatch: boolean; | ||
secretKeyMatch: boolean; | ||
envVarsMissing: boolean; | ||
keylessFileHasKeys: boolean; | ||
keylessPublishableKey: string; | ||
envPublishableKey: string; | ||
}; | ||
Comment on lines
+11
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not send raw keys; mask publishable keys and make fields optional Raw publishable keys shouldn’t be sent in telemetry. Also, casting/forcing missing values into strings can hide absence. Make fields optional and masked, and prefer interface for an extendable payload shape per guidelines. -type EventKeylessEnvDriftPayload = {
+interface EventKeylessEnvDriftPayload {
publicKeyMatch: boolean;
secretKeyMatch: boolean;
envVarsMissing: boolean;
keylessFileHasKeys: boolean;
- keylessPublishableKey: string;
- envPublishableKey: string;
-};
+ keylessPublishableKeyMasked?: string;
+ envPublishableKeyMasked?: string;
+} Add this helper outside the shown range: function maskKeyForTelemetry(key?: string): string | undefined {
if (!key) return undefined;
const head = key.slice(0, 8);
const tail = key.slice(-4);
return `${head}…${tail}`;
} 🤖 Prompt for AI Agents
|
||
|
||
/** | ||
* Gets the absolute path to the telemetry flag file. | ||
* | ||
* This file is used to track whether telemetry events have already been fired | ||
* to prevent duplicate event reporting during the application lifecycle. | ||
* | ||
* @returns The absolute path to the telemetry flag file in the project's .clerk/.tmp directory | ||
*/ | ||
function getTelemetryFlagFilePath(): string { | ||
return join(process.cwd(), TELEMETRY_FLAG_FILE); | ||
} | ||
|
||
/** | ||
* Attempts to create a telemetry flag file to mark that a telemetry event has been fired. | ||
* | ||
* This function uses the 'wx' flag to create the file atomically - it will only succeed | ||
* if the file doesn't already exist. This ensures that telemetry events are only fired | ||
* once per application lifecycle, preventing duplicate event reporting. | ||
* | ||
* @returns Promise<boolean> - Returns true if the flag file was successfully created (meaning | ||
* the event should be fired), false if the file already exists (meaning the event was | ||
* already fired) or if there was an error creating the file | ||
*/ | ||
async function tryMarkTelemetryEventAsFired(): Promise<boolean> { | ||
try { | ||
const flagFilePath = getTelemetryFlagFilePath(); | ||
const flagDirectory = dirname(flagFilePath); | ||
|
||
// Ensure the directory exists before attempting to write the file | ||
await fs.mkdir(flagDirectory, { recursive: true }); | ||
|
||
const flagData = { | ||
firedAt: new Date().toISOString(), | ||
event: EVENT_KEYLESS_ENV_DRIFT_DETECTED, | ||
}; | ||
await fs.writeFile(flagFilePath, JSON.stringify(flagData, null, 2), { flag: 'wx' }); | ||
return true; | ||
} catch (error: unknown) { | ||
if ((error as { code?: string })?.code === 'EEXIST') { | ||
return false; | ||
} | ||
console.warn('Failed to create telemetry flag file:', error); | ||
return false; | ||
} | ||
} | ||
|
||
/** | ||
* Detects and reports environment drift between keyless configuration and environment variables. | ||
* | ||
* This function compares the Clerk keys stored in the keyless configuration file (.clerk/clerk.json) | ||
* with the keys set in environment variables (NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY). | ||
* It only reports drift when there's an actual mismatch between existing keys, not when keys are simply missing. | ||
* | ||
* The function handles several scenarios and only reports drift in specific cases: | ||
* - **Normal keyless mode**: env vars missing but keyless file has keys → no drift (expected) | ||
* - **No configuration**: neither env vars nor keyless file have keys → no drift (nothing to compare) | ||
* - **Actual drift**: env vars exist and don't match keyless file keys → drift detected | ||
* - **Empty keyless file**: keyless file exists but has no keys → no drift (nothing to compare) | ||
* | ||
* Drift is only detected when: | ||
* 1. Both environment variables and keyless file contain keys | ||
* 2. The keys in environment variables don't match the keys in the keyless file | ||
* | ||
* Telemetry events are only fired once per application lifecycle using a flag file mechanism | ||
* to prevent duplicate reporting. | ||
* | ||
* @returns Promise<void> - Function completes silently, errors are logged but don't throw | ||
*/ | ||
export async function detectKeylessEnvDrift(): Promise<void> { | ||
// Only run on server side | ||
if (typeof window !== 'undefined') { | ||
return; | ||
} | ||
|
||
try { | ||
// Dynamically import server-side dependencies to avoid client-side issues | ||
const { safeParseClerkFile } = await import('./keyless-node.js'); | ||
|
||
// Read the keyless configuration file | ||
const keylessFile = safeParseClerkFile(); | ||
|
||
if (!keylessFile) { | ||
return; | ||
} | ||
|
||
// Get environment variables | ||
const envPublishableKey = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY; | ||
const envSecretKey = process.env.CLERK_SECRET_KEY; | ||
|
||
// Check the state of environment variables and keyless file | ||
const hasEnvVars = Boolean(envPublishableKey || envSecretKey); | ||
const keylessFileHasKeys = Boolean(keylessFile?.publishableKey && keylessFile?.secretKey); | ||
const envVarsMissing = !envPublishableKey && !envSecretKey; | ||
|
||
// Early return conditions - no drift to detect in these scenarios: | ||
if (!hasEnvVars && !keylessFileHasKeys) { | ||
// Neither env vars nor keyless file have keys - nothing to compare | ||
return; | ||
} | ||
|
||
if (envVarsMissing && keylessFileHasKeys) { | ||
// Environment variables are missing but keyless file has keys - this is normal for keyless mode | ||
return; | ||
} | ||
|
||
if (!keylessFileHasKeys) { | ||
// Keyless file doesn't have keys, so no drift can be detected | ||
return; | ||
} | ||
|
||
// Only proceed with drift detection if we have something meaningful to compare | ||
if (!hasEnvVars) { | ||
return; | ||
} | ||
|
||
// Compare keys only when both sides have values to compare | ||
const publicKeyMatch = Boolean( | ||
envPublishableKey && keylessFile.publishableKey && envPublishableKey === keylessFile.publishableKey, | ||
); | ||
|
||
const secretKeyMatch = Boolean(envSecretKey && keylessFile.secretKey && envSecretKey === keylessFile.secretKey); | ||
|
||
// Determine if there's an actual drift: | ||
// Drift occurs when we have env vars that don't match the keyless file keys | ||
const hasActualDrift = | ||
(envPublishableKey && keylessFile.publishableKey && !publicKeyMatch) || | ||
(envSecretKey && keylessFile.secretKey && !secretKeyMatch); | ||
|
||
// Only fire telemetry if there's an actual drift (not just missing keys) | ||
if (!hasActualDrift) { | ||
return; | ||
} | ||
|
||
const payload: EventKeylessEnvDriftPayload = { | ||
publicKeyMatch, | ||
secretKeyMatch, | ||
envVarsMissing, | ||
keylessFileHasKeys, | ||
keylessPublishableKey: keylessFile.publishableKey ?? '', | ||
envPublishableKey: envPublishableKey ?? '', | ||
}; | ||
heatlikeheatwave marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// Create a clerk client to access telemetry | ||
const clerkClient = createClerkClientWithOptions({ | ||
publishableKey: keylessFile.publishableKey, | ||
secretKey: keylessFile.secretKey, | ||
}); | ||
|
||
heatlikeheatwave marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const shouldFireEvent = await tryMarkTelemetryEventAsFired(); | ||
|
||
if (shouldFireEvent) { | ||
// Fire drift detected event only if we successfully created the flag | ||
const driftDetectedEvent: TelemetryEventRaw<EventKeylessEnvDriftPayload> = { | ||
event: EVENT_KEYLESS_ENV_DRIFT_DETECTED, | ||
eventSamplingRate: EVENT_SAMPLING_RATE, | ||
payload, | ||
}; | ||
|
||
clerkClient.telemetry?.record(driftDetectedEvent); | ||
} | ||
} catch (error) { | ||
// Silently handle errors to avoid breaking the application | ||
console.warn('Failed to detect keyless environment drift:', error); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.