|
| 1 | +/** |
| 2 | + * One-time runtime disclosure that Clerk collects telemetry from development instances. |
| 3 | + * |
| 4 | + * Replaces the previous `postinstall` script. Disclosure is intentionally surfaced |
| 5 | + * only on Node (server-side) so the noise profile matches the original postinstall |
| 6 | + * (terminal-only, dev-eyes-only). Browser consoles are not used because they are |
| 7 | + * frequently observed by non-developers (QA, screenshots, demos), and adding another |
| 8 | + * console warning is a common source of customer complaints. |
| 9 | + * |
| 10 | + * Known gap: pure browser-only setups with no server-side Clerk runtime (e.g. a Vite |
| 11 | + * SPA using `@clerk/clerk-react` or `@clerk/clerk-js` directly, without any Node/Edge |
| 12 | + * backend that imports `@clerk/shared`) will never hit this code path and therefore |
| 13 | + * see no in-band disclosure. This is an accepted trade-off: the original postinstall |
| 14 | + * already fired only once at install time and was easily missed, so the practical |
| 15 | + * delta is small. Authoritative disclosure for those setups lives in the Clerk |
| 16 | + * telemetry docs (https://clerk.com/docs/telemetry). Opt-out continues to work the |
| 17 | + * same way (`telemetry={false}` on `<ClerkProvider>` or the framework-specific |
| 18 | + * `*_CLERK_TELEMETRY_DISABLED` env var). |
| 19 | + * |
| 20 | + * Persistence is in-process via a `globalThis` Symbol, which survives Next.js HMR |
| 21 | + * module reloads. No filesystem access, no `node:` imports, no dynamic-code APIs, so |
| 22 | + * the module remains safe to bundle for Edge Runtime, Workers, and any browser path. |
| 23 | + * |
| 24 | + * All work is wrapped in try/catch. Failure to display the notice must never affect |
| 25 | + * the SDK. |
| 26 | + */ |
| 27 | + |
| 28 | +import { isTruthy } from '../underscore'; |
| 29 | +import { automatedEnvironmentVariables } from '../utils/runtimeEnvironment'; |
| 30 | + |
| 31 | +const PROCESS_FLAG = Symbol.for('@clerk/shared.telemetryNoticeShown'); |
| 32 | + |
| 33 | +const NOTICE_LINES = [ |
| 34 | + 'Attention: Clerk collects telemetry data from its SDKs when connected to development instances.', |
| 35 | + "The data collected is used to inform Clerk's product roadmap.", |
| 36 | + 'To learn more, including how to opt-out from the telemetry program, visit: https://clerk.com/docs/telemetry.', |
| 37 | +]; |
| 38 | + |
| 39 | +function isServerRuntime(): boolean { |
| 40 | + // Skip in browsers. |
| 41 | + if (typeof window !== 'undefined') { |
| 42 | + return false; |
| 43 | + } |
| 44 | + // Skip in Next.js Edge Runtime, which exposes a global `EdgeRuntime` marker. We detect via |
| 45 | + // this marker (rather than checking `process.versions`) because the Edge Runtime build-time |
| 46 | + // analyzer flags any reachable read of `process.versions` even when it sits behind a guard. |
| 47 | + if (typeof (globalThis as { EdgeRuntime?: string }).EdgeRuntime !== 'undefined') { |
| 48 | + return false; |
| 49 | + } |
| 50 | + return true; |
| 51 | +} |
| 52 | + |
| 53 | +// Server-only notice: read process.env directly rather than going through |
| 54 | +// getEnvVariable(), which would pull the multi-runtime env resolver into the |
| 55 | +// clerk.browser.js bundle (it ships there via TelemetryCollector). We reuse the |
| 56 | +// shared CI env-var list so the set of detected providers stays in one place. |
| 57 | +function isCI(): boolean { |
| 58 | + if (typeof process === 'undefined' || !process.env) { |
| 59 | + return false; |
| 60 | + } |
| 61 | + return automatedEnvironmentVariables.some(name => isTruthy(process.env[name])); |
| 62 | +} |
| 63 | + |
| 64 | +function hasSeen(): boolean { |
| 65 | + return Boolean((globalThis as Record<symbol, unknown>)[PROCESS_FLAG]); |
| 66 | +} |
| 67 | + |
| 68 | +function markSeen(): void { |
| 69 | + (globalThis as Record<symbol, unknown>)[PROCESS_FLAG] = true; |
| 70 | +} |
| 71 | + |
| 72 | +function printNotice(): void { |
| 73 | + if (typeof console === 'undefined' || typeof console.log !== 'function') { |
| 74 | + return; |
| 75 | + } |
| 76 | + for (const line of NOTICE_LINES) { |
| 77 | + console.log(line); |
| 78 | + } |
| 79 | + console.log(''); |
| 80 | +} |
| 81 | + |
| 82 | +export type MaybeShowTelemetryNoticeOptions = { |
| 83 | + /** |
| 84 | + * Skip the notice entirely. Used when the caller has already determined that no |
| 85 | + * telemetry will be sent (e.g. opt-out, non-development instance), in which case |
| 86 | + * there is nothing to disclose. |
| 87 | + */ |
| 88 | + skip?: boolean; |
| 89 | +}; |
| 90 | + |
| 91 | +/** |
| 92 | + * Display the one-time telemetry disclosure on server runtimes if it has not already been |
| 93 | + * shown in this process. Browser and Edge Runtime callers are silently skipped. Never throws. |
| 94 | + */ |
| 95 | +export function maybeShowTelemetryNotice(options: MaybeShowTelemetryNoticeOptions = {}): void { |
| 96 | + if (options.skip) { |
| 97 | + return; |
| 98 | + } |
| 99 | + try { |
| 100 | + if (!isServerRuntime()) { |
| 101 | + return; |
| 102 | + } |
| 103 | + if (isCI()) { |
| 104 | + return; |
| 105 | + } |
| 106 | + if (hasSeen()) { |
| 107 | + return; |
| 108 | + } |
| 109 | + printNotice(); |
| 110 | + markSeen(); |
| 111 | + } catch { |
| 112 | + // never let disclosure break the SDK |
| 113 | + } |
| 114 | +} |
| 115 | + |
| 116 | +/** |
| 117 | + * Test-only: clear the in-process flag so the next call re-runs the gating logic. |
| 118 | + * |
| 119 | + * @internal |
| 120 | + */ |
| 121 | +export function __resetTelemetryNoticeForTests(): void { |
| 122 | + delete (globalThis as Record<symbol, unknown>)[PROCESS_FLAG]; |
| 123 | +} |
0 commit comments