Skip to content

Commit

Permalink
feat(nextjs): Log usage of protect() in pages,routes or actions
Browse files Browse the repository at this point in the history
  • Loading branch information
panteliselef committed Oct 23, 2024
1 parent 0ee4640 commit e8aff3f
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 8 deletions.
8 changes: 8 additions & 0 deletions .changeset/lazy-hounds-provide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@clerk/nextjs": minor
"@clerk/shared": minor
"@clerk/types": minor
---

Telemetry: Log usage of `auth.protect()` inside pages, routes, or actions.
- Only applications with `experimental.after` turned on, otherwise it will be a noop.
6 changes: 4 additions & 2 deletions packages/nextjs/src/server/nextFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ type Fetcher = typeof globalThis.fetch;
*/
type NextFetcher = Fetcher & {
readonly __nextPatched: true;
readonly __nextGetStaticStore: () => { getStore: () => StaticGenerationAsyncStorage | undefined };
readonly __nextGetStaticStore: () => { getStore: () => WorkStoreAsyncStorage | undefined };
};

/**
* Full type can be found https://github.com/vercel/next.js/blob/6185444e0a944a82e7719ac37dad8becfed86acd/packages/next/src/client/components/static-generation-async-storage.external.ts#L4
*/
interface StaticGenerationAsyncStorage {
interface WorkStoreAsyncStorage {
/**
* Available for Next 14
*/
Expand All @@ -20,6 +20,8 @@ interface StaticGenerationAsyncStorage {
* Available for Next 15
*/
readonly page?: string;

readonly afterContext?: any;
}

function isNextFetcher(fetch: Fetcher | NextFetcher): fetch is NextFetcher {
Expand Down
28 changes: 28 additions & 0 deletions packages/nextjs/src/server/safe_after.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { isNextFetcher } from './nextFetcher';

export type AfterTask<T = unknown> = Promise<T> | AfterCallback<T>;
export type AfterCallback<T = unknown> = () => T | Promise<T>;

export async function safe_after<T>(task: AfterTask<T>): Promise<void> {
const __fetch = globalThis.fetch;

if (!isNextFetcher(__fetch)) {
return;
}

const { afterContext } = __fetch.__nextGetStaticStore().getStore() || {};

if (!afterContext) {
// If the application does not have the experimental flag turned on it needs to be a noop, otherwise Next.js will throw
return;
}

// @ts-ignore
const { unstable_after } = (await import('next/server.js')) || {};
if (!unstable_after) {
// Application uses a nextjs version that does not export the utility
return;
}

unstable_after(task);
}
26 changes: 20 additions & 6 deletions packages/shared/src/telemetry/collector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,20 @@ export class TelemetryCollector implements TelemetryCollectorInterface {
this.#scheduleFlush();
}

async recordAsync(event: TelemetryEventRaw): Promise<void> {
const preparedPayload = this.#preparePayload(event.event, event.payload);

this.#logEvent(preparedPayload.event, preparedPayload);

if (!this.#shouldRecord(preparedPayload, event.eventSamplingRate)) {
return;
}

this.#buffer.push(preparedPayload);

await this.#flush();
}

#shouldRecord(preparedPayload: TelemetryEvent, eventSamplingRate?: number) {
return this.isEnabled && !this.isDebug && this.#shouldBeSampled(preparedPayload, eventSamplingRate);
}
Expand All @@ -148,7 +162,7 @@ export class TelemetryCollector implements TelemetryCollectorInterface {
#scheduleFlush(): void {
// On the server, we want to flush immediately as we have less guarantees about the lifecycle of the process
if (typeof window === 'undefined') {
this.#flush();
void this.#flush();
return;
}

Expand All @@ -160,7 +174,7 @@ export class TelemetryCollector implements TelemetryCollectorInterface {
const cancel = typeof cancelIdleCallback !== 'undefined' ? cancelIdleCallback : clearTimeout;
cancel(this.#pendingFlush);
}
this.#flush();
void this.#flush();
return;
}

Expand All @@ -171,18 +185,18 @@ export class TelemetryCollector implements TelemetryCollectorInterface {

if ('requestIdleCallback' in window) {
this.#pendingFlush = requestIdleCallback(() => {
this.#flush();
void this.#flush();
});
} else {
// This is not an ideal solution, but it at least waits until the next tick
this.#pendingFlush = setTimeout(() => {
this.#flush();
void this.#flush();
}, 0);
}
}

#flush(): void {
fetch(new URL('/v1/event', this.#config.endpoint), {
#flush(): Promise<unknown> {
return fetch(new URL('/v1/event', this.#config.endpoint), {
method: 'POST',
// TODO: We send an array here with that idea that we can eventually send multiple events.
body: JSON.stringify({
Expand Down
3 changes: 3 additions & 0 deletions packages/types/src/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,8 @@ export type TelemetryEventRaw<Payload = TelemetryEvent['payload']> = {
export interface TelemetryCollector {
isEnabled: boolean;
isDebug: boolean;

record(event: TelemetryEventRaw): void;

recordAsync(event: TelemetryEventRaw): Promise<void>;
}

0 comments on commit e8aff3f

Please sign in to comment.