Enhance client analytics event validation and type handling

- Introduced a new validation function, `isValidClientAnalyticsEventType`, to ensure client analytics events do not use server-only event types.
- Updated the analytics validation logic to differentiate between client and server event types, improving clarity and security in event handling.
- Adjusted documentation to reflect the new validation rules for client-sent analytics events.

These changes strengthen the integrity of analytics event processing and enhance the overall structure of event type management.
This commit is contained in:
mantrakp04 2026-03-25 19:36:00 -07:00
parent f93b14c2c9
commit df06834cd7
4 changed files with 30 additions and 9 deletions

View File

@ -1,4 +1,4 @@
import { isValidPublicAnalyticsEventType, PUBLIC_STACK_ANALYTICS_EVENT_TYPE_LIST, UUID_RE } from "@/lib/analytics-validation";
import { isValidClientAnalyticsEventType, isValidPublicAnalyticsEventType, PUBLIC_STACK_ANALYTICS_EVENT_TYPE_LIST, UUID_RE } from "@/lib/analytics-validation";
import { insertAnalyticsEvents } from "@/lib/events";
import { findRecentSessionReplay } from "@/lib/session-replays";
import { getPrismaClientForTenancy } from "@/prisma-client";
@ -69,6 +69,9 @@ export const POST = createSmartRouteHandler({
if (auth.type === "client" && body.events.some((event) => event.user_id != null || event.team_id != null)) {
throw new StatusError(StatusError.BadRequest, "Client analytics events cannot override user_id or team_id");
}
if (auth.type === "client" && body.events.some((event) => !isValidClientAnalyticsEventType(event.event_type))) {
throw new StatusError(StatusError.BadRequest, "Client analytics events cannot use server-only event types");
}
const projectId = auth.tenancy.project.id;
const branchId = auth.tenancy.branchId;

View File

@ -12,9 +12,16 @@ export const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[089ab][0-9a-f
export const CUSTOM_ANALYTICS_NAME_RE = /^[A-Za-z0-9._:-]+$/;
const PUBLIC_STACK_ANALYTICS_EVENT_TYPE_SET = new Set<string>(AUTO_CAPTURED_ANALYTICS_EVENT_TYPES);
/** $-prefixed event types that browser clients may send. */
const CLIENT_ANALYTICS_EVENT_TYPE_SET = new Set<string>(AUTO_CAPTURED_ANALYTICS_EVENT_TYPES);
export const PUBLIC_STACK_ANALYTICS_EVENT_TYPE_LIST = AUTO_CAPTURED_ANALYTICS_EVENT_TYPES
/** $-prefixed event types that only server/admin auth may send. */
const SERVER_ONLY_ANALYTICS_EVENT_TYPES = ["$request"] as const;
const ALL_PUBLIC_ANALYTICS_EVENT_TYPES = [...AUTO_CAPTURED_ANALYTICS_EVENT_TYPES, ...SERVER_ONLY_ANALYTICS_EVENT_TYPES];
const ALL_PUBLIC_ANALYTICS_EVENT_TYPE_SET = new Set<string>(ALL_PUBLIC_ANALYTICS_EVENT_TYPES);
export const PUBLIC_STACK_ANALYTICS_EVENT_TYPE_LIST = ALL_PUBLIC_ANALYTICS_EVENT_TYPES
.map((eventType) => `"${eventType}"`)
.join(", ");
@ -38,10 +45,17 @@ export function isValidCustomAnalyticsName(
}
/**
* Pre-built validator for event_type.
* Validates event_type for client auth (browser auto-captured + custom names).
*/
export function isValidClientAnalyticsEventType(eventType: string | undefined): boolean {
return isValidCustomAnalyticsName(eventType, CLIENT_ANALYTICS_EVENT_TYPE_SET);
}
/**
* Validates event_type for server/admin auth (all public types + custom names).
*/
export function isValidPublicAnalyticsEventType(eventType: string | undefined): boolean {
return isValidCustomAnalyticsName(eventType, PUBLIC_STACK_ANALYTICS_EVENT_TYPE_SET);
return isValidCustomAnalyticsName(eventType, ALL_PUBLIC_ANALYTICS_EVENT_TYPE_SET);
}
/**

View File

@ -40,8 +40,10 @@ export type AnalyticsBatchSpan = {
};
/**
* Auto-captured (Stack-managed) analytics event types that clients are allowed to send.
* These are the only `$`-prefixed event types permitted from the client.
* Auto-captured (Stack-managed) analytics event types that browser clients are
* allowed to send. These are the only `$`-prefixed event types permitted from
* client auth. Server-only types like `$request` are validated separately on
* the backend.
*
* Span types have no public `$`-prefixed types all `$`-prefixed spans
* ($session-replay, $session-replay-segment) are created server-side only.
@ -59,7 +61,6 @@ export const AUTO_CAPTURED_ANALYTICS_EVENT_TYPES = [
"$copy",
"$paste",
"$error",
"$request",
] as const;
export type AutoCapturedAnalyticsEventType = typeof AUTO_CAPTURED_ANALYTICS_EVENT_TYPES[number];

View File

@ -10,6 +10,9 @@ export type AnalyticsReplayLinkOptions = {
const autoCapturedAnalyticsEventTypeSet = new Set<string>(AUTO_CAPTURED_ANALYTICS_EVENT_TYPES);
/** Server-only $-prefixed event types (e.g. $request from middleware). Not client-sendable. */
const serverOnlyAnalyticsEventTypes = new Set<string>(["$request"]);
export function assertValidAnalyticsEventName(
eventType: string,
options: { allowAutoCapturedReservedType?: boolean } = {},
@ -17,7 +20,7 @@ export function assertValidAnalyticsEventName(
if (!eventType) {
throw new StackAssertionError("Analytics event type must not be empty");
}
if (options.allowAutoCapturedReservedType && autoCapturedAnalyticsEventTypeSet.has(eventType)) {
if (options.allowAutoCapturedReservedType && (autoCapturedAnalyticsEventTypeSet.has(eventType) || serverOnlyAnalyticsEventTypes.has(eventType))) {
return;
}
if (eventType.startsWith("$")) {