diff --git a/packages/web/package.json b/packages/web/package.json index 906a561..510d49e 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "@vercel/analytics", - "version": "1.2.2", + "version": "1.3.0", "description": "Gain real-time traffic insights with Vercel Web Analytics", "keywords": [ "analytics", diff --git a/packages/web/src/generic.ts b/packages/web/src/generic.ts index a0bf1ea..6009ed5 100644 --- a/packages/web/src/generic.ts +++ b/packages/web/src/generic.ts @@ -1,6 +1,10 @@ import { name as packageName, version } from '../package.json'; import { initQueue } from './queue'; -import type { AllowedPropertyValues, AnalyticsProps } from './types'; +import type { + AllowedPropertyValues, + AnalyticsProps, + FlagsDataInput, +} from './types'; import { isBrowser, parseProperties, @@ -89,7 +93,10 @@ function inject( */ function track( name: string, - properties?: Record + properties?: Record, + options?: { + flags?: FlagsDataInput; + } ): void { if (!isBrowser()) { const msg = @@ -106,7 +113,7 @@ function track( } if (!properties) { - window.va?.('event', { name }); + window.va?.('event', { name, options }); return; } @@ -118,6 +125,7 @@ function track( window.va?.('event', { name, data: props, + options, }); } catch (err) { if (err instanceof Error && isDevelopment()) { diff --git a/packages/web/src/server/index.ts b/packages/web/src/server/index.ts index dd645b1..eeb0b6f 100644 --- a/packages/web/src/server/index.ts +++ b/packages/web/src/server/index.ts @@ -1,5 +1,9 @@ /* eslint-disable no-console -- Allow logging on the server */ -import type { AllowedPropertyValues } from '../types'; +import type { + AllowedPropertyValues, + FlagsDataInput, + PlainFlags, +} from '../types'; import { isProduction, parseProperties } from '../utils'; type HeadersObject = Record; @@ -10,20 +14,21 @@ function isHeaders(headers?: AllowedHeaders): headers is Headers { return typeof (headers as HeadersObject).entries === 'function'; } -interface ContextWithRequest { - request: { headers: AllowedHeaders }; +interface Options { + flags?: FlagsDataInput; + headers?: AllowedHeaders; + request?: { headers: AllowedHeaders }; } -interface ContextWithHeaders { - headers: AllowedHeaders; -} - -type Context = ContextWithRequest | ContextWithHeaders; interface RequestContext { get: () => { headers: Record; url: string; waitUntil?: (promise: Promise) => void; + flags?: { + getValues: () => PlainFlags; + reportValue: (key: string, value: unknown) => void; + }; }; } @@ -33,7 +38,7 @@ const logPrefix = '[Vercel Web Analytics]'; export async function track( eventName: string, properties?: Record, - context?: Context + options?: Options ): Promise { const ENDPOINT = process.env.VERCEL_WEB_ANALYTICS_ENDPOINT || process.env.VERCEL_URL; @@ -75,10 +80,10 @@ export async function track( let headers: AllowedHeaders | undefined; - if (context && 'headers' in context) { - headers = context.headers; - } else if (context?.request) { - headers = context.request.headers; + if (options && 'headers' in options) { + headers = options.headers; + } else if (options?.request) { + headers = options.request.headers; } else if (requestContext?.headers) { // not explicitly passed in context, so take it from async storage headers = requestContext.headers; @@ -104,6 +109,7 @@ export async function track( r: '', en: eventName, ed: props, + f: safeGetFlags(options?.flags, requestContext), }; const hasHeaders = Boolean(headers); @@ -135,7 +141,7 @@ export async function track( body: JSON.stringify(body), method: 'POST', }) - // We want to always consume to body; some cloud providers track fetch concurrency + // We want to always consume the body; some cloud providers track fetch concurrency // and may not release the connection until the body is consumed. .then((response) => response.text()) .catch((err: unknown) => { @@ -157,3 +163,38 @@ export async function track( console.error(err); } } + +function safeGetFlags( + flags: Options['flags'], + requestContext?: ReturnType +): + | { + p: PlainFlags; + } + | undefined { + try { + if (!requestContext || !flags) return; + // In the case plain flags are passed, just return them + if (!Array.isArray(flags)) { + return { p: flags }; + } + + const plainFlags: Record = {}; + // returns all available plain flags + const resolvedPlainFlags = requestContext.flags?.getValues() ?? {}; + + for (const flag of flags) { + if (typeof flag === 'string') { + // only picks the desired flags + plainFlags[flag] = resolvedPlainFlags[flag]; + } else { + // merge user-provided values with resolved values + Object.assign(plainFlags, flag); + } + } + + return { p: plainFlags }; + } catch { + /* empty */ + } +} diff --git a/packages/web/src/types.ts b/packages/web/src/types.ts index aab7ee0..6301022 100644 --- a/packages/web/src/types.ts +++ b/packages/web/src/types.ts @@ -39,3 +39,6 @@ declare global { vam?: Mode; } } + +export type PlainFlags = Record; +export type FlagsDataInput = (string | PlainFlags)[] | PlainFlags;