diff --git a/client/src/api/admin/endpoints/sites.ts b/client/src/api/admin/endpoints/sites.ts index 2bf35340e..4b1ee6b52 100644 --- a/client/src/api/admin/endpoints/sites.ts +++ b/client/src/api/admin/endpoints/sites.ts @@ -15,6 +15,7 @@ export type SiteResponse = { saltUserIds: boolean; blockBots: boolean; firstPartyProxy?: boolean; + persistentClientIds?: boolean; isOwner: boolean; // Analytics features sessionReplay?: boolean; @@ -148,6 +149,7 @@ export function updateSiteConfig( saltUserIds?: boolean; blockBots?: boolean; firstPartyProxy?: boolean; + persistentClientIds?: boolean; excludedIPs?: string[]; excludedCountries?: string[]; excludedPaths?: string[]; diff --git a/client/src/components/SiteSettings/GeneralTab.tsx b/client/src/components/SiteSettings/GeneralTab.tsx index 74a9e9f3a..98973a012 100644 --- a/client/src/components/SiteSettings/GeneralTab.tsx +++ b/client/src/components/SiteSettings/GeneralTab.tsx @@ -78,6 +78,7 @@ export function GeneralTab({ siteMetadata, disabled = false, onClose, onPublicCh saltUserIds: siteMetadata.saltUserIds || false, blockBots: siteMetadata.blockBots || false, firstPartyProxy: siteMetadata.firstPartyProxy || false, + persistentClientIds: siteMetadata.persistentClientIds || false, trackIp: siteMetadata.trackIp ?? false, }); @@ -206,9 +207,12 @@ export function GeneralTab({ siteMetadata, disabled = false, onClose, onPublicCh { id: "saltUserIds", label: t("User ID Salting"), - description: t("User IDs will be salted with a daily rotating key for enhanced privacy"), + description: toggleStates.persistentClientIds + ? t("Disable Persistent Client IDs first — the two settings are mutually exclusive.") + : t("User IDs will be salted with a daily rotating key for enhanced privacy"), value: toggleStates.saltUserIds, key: "saltUserIds", + disabled: toggleStates.persistentClientIds, enabledMessage: t("User ID salting enabled"), disabledMessage: t("User ID salting disabled"), }, @@ -232,6 +236,20 @@ export function GeneralTab({ siteMetadata, disabled = false, onClose, onPublicCh enabledMessage: t("First-party proxy mode enabled"), disabledMessage: t("First-party proxy mode disabled"), }, + { + id: "persistentClientIds", + label: t("Persistent Client IDs"), + description: toggleStates.saltUserIds + ? t("Disable User ID Salting first — the two settings are mutually exclusive.") + : t( + "Store a persistent identifier in the visitor's browser for more accurate user identification than the cookieless IP+UA fingerprint. This requires your own consent banner, since it stores an identifier on the visitor's device." + ), + value: toggleStates.persistentClientIds, + key: "persistentClientIds", + disabled: toggleStates.saltUserIds, + enabledMessage: t("Persistent client IDs enabled"), + disabledMessage: t("Persistent client IDs disabled"), + }, { id: "trackIp", label: t("Track IP Address"), diff --git a/server/drizzle/0013_dizzy_celestials.sql b/server/drizzle/0013_dizzy_celestials.sql new file mode 100644 index 000000000..8e929cbc7 --- /dev/null +++ b/server/drizzle/0013_dizzy_celestials.sql @@ -0,0 +1 @@ +ALTER TABLE "sites" ADD COLUMN "persistent_client_ids" boolean DEFAULT false; \ No newline at end of file diff --git a/server/drizzle/meta/0013_snapshot.json b/server/drizzle/meta/0013_snapshot.json new file mode 100644 index 000000000..7ff00e924 --- /dev/null +++ b/server/drizzle/meta/0013_snapshot.json @@ -0,0 +1,3677 @@ +{ + "id": "dc3ff35c-8c16-477c-9581-4d06359fb9fa", + "prevId": "43a2b4e7-703c-4e99-8f84-33e62a6bd3bb", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "accountId": { + "name": "accountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerId": { + "name": "providerId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idToken": { + "name": "idToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessTokenExpiresAt": { + "name": "accessTokenExpiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refreshTokenExpiresAt": { + "name": "refreshTokenExpiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.active_sessions": { + "name": "active_sessions", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "site_id": { + "name": "site_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start_time": { + "name": "start_time", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "last_activity": { + "name": "last_activity", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_regions": { + "name": "agent_regions", + "schema": "", + "columns": { + "code": { + "name": "code", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint_url": { + "name": "endpoint_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "last_health_check": { + "name": "last_health_check", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_healthy": { + "name": "is_healthy", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "referenceId": { + "name": "referenceId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refillInterval": { + "name": "refillInterval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refillAmount": { + "name": "refillAmount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "lastRefillAt": { + "name": "lastRefillAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "rateLimitEnabled": { + "name": "rateLimitEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "rateLimitTimeWindow": { + "name": "rateLimitTimeWindow", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rateLimitMax": { + "name": "rateLimitMax", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "requestCount": { + "name": "requestCount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "lastRequest": { + "name": "lastRequest", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "configId": { + "name": "configId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cancellation_feedback": { + "name": "cancellation_feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason_details": { + "name": "reason_details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "retention_offer_shown": { + "name": "retention_offer_shown", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "retention_offer_accepted": { + "name": "retention_offer_accepted", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan_name_at_cancellation": { + "name": "plan_name_at_cancellation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "monthly_event_count_at_cancellation": { + "name": "monthly_event_count_at_cancellation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboards": { + "name": "dashboards", + "schema": "", + "columns": { + "dashboard_id": { + "name": "dashboard_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "site_id": { + "name": "site_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"cards\":[]}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboards_site_id_sites_site_id_fk": { + "name": "dashboards_site_id_sites_site_id_fk", + "tableFrom": "dashboards", + "tableTo": "sites", + "columnsFrom": [ + "site_id" + ], + "columnsTo": [ + "site_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "dashboards_user_id_user_id_fk": { + "name": "dashboards_user_id_user_id_fk", + "tableFrom": "dashboards", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.experiments": { + "name": "experiments", + "schema": "", + "columns": { + "experiment_id": { + "name": "experiment_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "site_id": { + "name": "site_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "feature_flag_id": { + "name": "feature_flag_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "primary_goal_id": { + "name": "primary_goal_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hypothesis": { + "name": "hypothesis", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "winning_variant": { + "name": "winning_variant", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "experiments_site_idx": { + "name": "experiments_site_idx", + "columns": [ + { + "expression": "site_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "experiments_feature_flag_idx": { + "name": "experiments_feature_flag_idx", + "columns": [ + { + "expression": "feature_flag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "experiments_primary_goal_idx": { + "name": "experiments_primary_goal_idx", + "columns": [ + { + "expression": "primary_goal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "experiments_site_id_sites_site_id_fk": { + "name": "experiments_site_id_sites_site_id_fk", + "tableFrom": "experiments", + "tableTo": "sites", + "columnsFrom": [ + "site_id" + ], + "columnsTo": [ + "site_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "experiments_feature_flag_id_feature_flags_flag_id_fk": { + "name": "experiments_feature_flag_id_feature_flags_flag_id_fk", + "tableFrom": "experiments", + "tableTo": "feature_flags", + "columnsFrom": [ + "feature_flag_id" + ], + "columnsTo": [ + "flag_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "experiments_primary_goal_id_goals_goal_id_fk": { + "name": "experiments_primary_goal_id_goals_goal_id_fk", + "tableFrom": "experiments", + "tableTo": "goals", + "columnsFrom": [ + "primary_goal_id" + ], + "columnsTo": [ + "goal_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "experiments_site_flag_unique": { + "name": "experiments_site_flag_unique", + "nullsNotDistinct": false, + "columns": [ + "site_id", + "feature_flag_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "experiments_status_check": { + "name": "experiments_status_check", + "value": "status IN ('draft', 'running', 'paused', 'completed')" + } + }, + "isRLSEnabled": false + }, + "public.feature_flags": { + "name": "feature_flags", + "schema": "", + "columns": { + "flag_id": { + "name": "flag_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "site_id": { + "name": "site_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "runtime": { + "name": "runtime", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'client'" + }, + "flag_type": { + "name": "flag_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'boolean'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "variants": { + "name": "variants", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "rollout_percentage": { + "name": "rollout_percentage", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "rules": { + "name": "rules", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "condition_sets": { + "name": "condition_sets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "md5(random()::text || clock_timestamp()::text)" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "feature_flags_site_idx": { + "name": "feature_flags_site_idx", + "columns": [ + { + "expression": "site_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "feature_flags_site_id_sites_site_id_fk": { + "name": "feature_flags_site_id_sites_site_id_fk", + "tableFrom": "feature_flags", + "tableTo": "sites", + "columnsFrom": [ + "site_id" + ], + "columnsTo": [ + "site_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "feature_flags_site_key_unique": { + "name": "feature_flags_site_key_unique", + "nullsNotDistinct": false, + "columns": [ + "site_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "feature_flags_rollout_check": { + "name": "feature_flags_rollout_check", + "value": "rollout_percentage >= 0 AND rollout_percentage <= 100" + }, + "feature_flags_runtime_check": { + "name": "feature_flags_runtime_check", + "value": "runtime IN ('client', 'server', 'both')" + }, + "feature_flags_type_check": { + "name": "feature_flags_type_check", + "value": "flag_type IN ('boolean', 'multivariate', 'remote_config')" + } + }, + "isRLSEnabled": false + }, + "public.funnels": { + "name": "funnels", + "schema": "", + "columns": { + "report_id": { + "name": "report_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "site_id": { + "name": "site_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "funnels_site_id_sites_site_id_fk": { + "name": "funnels_site_id_sites_site_id_fk", + "tableFrom": "funnels", + "tableTo": "sites", + "columnsFrom": [ + "site_id" + ], + "columnsTo": [ + "site_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "funnels_user_id_user_id_fk": { + "name": "funnels_user_id_user_id_fk", + "tableFrom": "funnels", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goals": { + "name": "goals", + "schema": "", + "columns": { + "goal_id": { + "name": "goal_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "site_id": { + "name": "site_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "goal_type": { + "name": "goal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "goals_site_id_sites_site_id_fk": { + "name": "goals_site_id_sites_site_id_fk", + "tableFrom": "goals", + "tableTo": "sites", + "columnsFrom": [ + "site_id" + ], + "columnsTo": [ + "site_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gsc_connections": { + "name": "gsc_connections", + "schema": "", + "columns": { + "site_id": { + "name": "site_id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "gsc_property_url": { + "name": "gsc_property_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "gsc_connections_site_id_sites_site_id_fk": { + "name": "gsc_connections_site_id_sites_site_id_fk", + "tableFrom": "gsc_connections", + "tableTo": "sites", + "columnsFrom": [ + "site_id" + ], + "columnsTo": [ + "site_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.import_status": { + "name": "import_status", + "schema": "", + "columns": { + "import_id": { + "name": "import_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "site_id": { + "name": "site_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "import_platform_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "imported_events": { + "name": "imported_events", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped_events": { + "name": "skipped_events", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "invalid_events": { + "name": "invalid_events", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "import_status_site_id_sites_site_id_fk": { + "name": "import_status_site_id_sites_site_id_fk", + "tableFrom": "import_status", + "tableTo": "sites", + "columnsFrom": [ + "site_id" + ], + "columnsTo": [ + "site_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "import_status_organization_id_organization_id_fk": { + "name": "import_status_organization_id_organization_id_fk", + "tableFrom": "import_status", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviterId": { + "name": "inviterId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "has_restricted_site_access": { + "name": "has_restricted_site_access", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "site_ids": { + "name": "site_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "teamId": { + "name": "teamId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_inviterId_user_id_fk": { + "name": "invitation_inviterId_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviterId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "invitation_organizationId_organization_id_fk": { + "name": "invitation_organizationId_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "invitation_teamId_team_id_fk": { + "name": "invitation_teamId_team_id_fk", + "tableFrom": "invitation", + "tableTo": "team", + "columnsFrom": [ + "teamId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "has_restricted_site_access": { + "name": "has_restricted_site_access", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "member_organizationId_organization_id_fk": { + "name": "member_organizationId_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "member_userId_user_id_fk": { + "name": "member_userId_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member_site_access": { + "name": "member_site_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "member_id": { + "name": "member_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "site_id": { + "name": "site_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "member_site_access_member_idx": { + "name": "member_site_access_member_idx", + "columns": [ + { + "expression": "member_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_site_access_site_idx": { + "name": "member_site_access_site_idx", + "columns": [ + { + "expression": "site_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_site_access_member_id_member_id_fk": { + "name": "member_site_access_member_id_member_id_fk", + "tableFrom": "member_site_access", + "tableTo": "member", + "columnsFrom": [ + "member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_site_access_site_id_sites_site_id_fk": { + "name": "member_site_access_site_id_sites_site_id_fk", + "tableFrom": "member_site_access", + "tableTo": "sites", + "columnsFrom": [ + "site_id" + ], + "columnsTo": [ + "site_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_site_access_created_by_user_id_fk": { + "name": "member_site_access_created_by_user_id_fk", + "tableFrom": "member_site_access", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "member_site_access_unique": { + "name": "member_site_access_unique", + "nullsNotDistinct": false, + "columns": [ + "member_id", + "site_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channels": { + "name": "notification_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "monitor_ids": { + "name": "monitor_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "trigger_events": { + "name": "trigger_events", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[\"down\",\"recovery\"]'::jsonb" + }, + "cooldown_minutes": { + "name": "cooldown_minutes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5 + }, + "last_notified_at": { + "name": "last_notified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_organization_id_organization_id_fk": { + "name": "notification_channels_organization_id_organization_id_fk", + "tableFrom": "notification_channels", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "notification_channels_created_by_user_id_fk": { + "name": "notification_channels_created_by_user_id_fk", + "tableFrom": "notification_channels", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauthAccessToken": { + "name": "oauthAccessToken", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessTokenExpiresAt": { + "name": "accessTokenExpiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "refreshTokenExpiresAt": { + "name": "refreshTokenExpiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "clientId": { + "name": "clientId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "oauthAccessToken_clientId_oauthApplication_clientId_fk": { + "name": "oauthAccessToken_clientId_oauthApplication_clientId_fk", + "tableFrom": "oauthAccessToken", + "tableTo": "oauthApplication", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "clientId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauthAccessToken_userId_user_id_fk": { + "name": "oauthAccessToken_userId_user_id_fk", + "tableFrom": "oauthAccessToken", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauthAccessToken_accessToken_unique": { + "name": "oauthAccessToken_accessToken_unique", + "nullsNotDistinct": false, + "columns": [ + "accessToken" + ] + }, + "oauthAccessToken_refreshToken_unique": { + "name": "oauthAccessToken_refreshToken_unique", + "nullsNotDistinct": false, + "columns": [ + "refreshToken" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauthApplication": { + "name": "oauthApplication", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "clientId": { + "name": "clientId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "clientSecret": { + "name": "clientSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirectUrls": { + "name": "redirectUrls", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "oauthApplication_userId_user_id_fk": { + "name": "oauthApplication_userId_user_id_fk", + "tableFrom": "oauthApplication", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauthApplication_clientId_unique": { + "name": "oauthApplication_clientId_unique", + "nullsNotDistinct": false, + "columns": [ + "clientId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauthConsent": { + "name": "oauthConsent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "clientId": { + "name": "clientId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consentGiven": { + "name": "consentGiven", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "oauthConsent_clientId_oauthApplication_clientId_fk": { + "name": "oauthConsent_clientId_oauthApplication_clientId_fk", + "tableFrom": "oauthConsent", + "tableTo": "oauthApplication", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "clientId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauthConsent_userId_user_id_fk": { + "name": "oauthConsent_userId_user_id_fk", + "tableFrom": "oauthConsent", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripeCustomerId": { + "name": "stripeCustomerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "monthlyEventCount": { + "name": "monthlyEventCount", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "overMonthlyLimit": { + "name": "overMonthlyLimit", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "approachingLimitNotifiedPeriodStart": { + "name": "approachingLimitNotifiedPeriodStart", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "planOverride": { + "name": "planOverride", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_plan": { + "name": "custom_plan", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userAgent": { + "name": "userAgent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonatedBy": { + "name": "impersonatedBy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "activeOrganizationId": { + "name": "activeOrganizationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "activeTeamId": { + "name": "activeTeamId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sites": { + "name": "sites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "site_id": { + "name": "site_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "embed_enabled": { + "name": "embed_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "saltUserIds": { + "name": "saltUserIds", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "blockBots": { + "name": "blockBots", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "first_party_proxy": { + "name": "first_party_proxy", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "persistent_client_ids": { + "name": "persistent_client_ids", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "excluded_ips": { + "name": "excluded_ips", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "excluded_countries": { + "name": "excluded_countries", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "excluded_paths": { + "name": "excluded_paths", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "excluded_hostnames": { + "name": "excluded_hostnames", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "excluded_user_agents": { + "name": "excluded_user_agents", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "sessionReplay": { + "name": "sessionReplay", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "webVitals": { + "name": "webVitals", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "trackErrors": { + "name": "trackErrors", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "trackOutbound": { + "name": "trackOutbound", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "trackUrlParams": { + "name": "trackUrlParams", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "trackInitialPageView": { + "name": "trackInitialPageView", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "trackSpaNavigation": { + "name": "trackSpaNavigation", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "trackIp": { + "name": "trackIp", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "trackButtonClicks": { + "name": "trackButtonClicks", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "trackCopy": { + "name": "trackCopy", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "trackFormInteractions": { + "name": "trackFormInteractions", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_link_key": { + "name": "private_link_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "sites_created_by_user_id_fk": { + "name": "sites_created_by_user_id_fk", + "tableFrom": "sites", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "sites_organization_id_organization_id_fk": { + "name": "sites_organization_id_organization_id_fk", + "tableFrom": "sites", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "sites_type_check": { + "name": "sites_type_check", + "value": "\"sites\".\"type\" IS NULL OR \"sites\".\"type\" IN ('web', 'mobile')" + } + }, + "isRLSEnabled": false + }, + "public.team": { + "name": "team", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "team_organizationId_organization_id_fk": { + "name": "team_organizationId_organization_id_fk", + "tableFrom": "team", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teamMember": { + "name": "teamMember", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "teamId": { + "name": "teamId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "teamMember_teamId_team_id_fk": { + "name": "teamMember_teamId_team_id_fk", + "tableFrom": "teamMember", + "tableTo": "team", + "columnsFrom": [ + "teamId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "teamMember_userId_user_id_fk": { + "name": "teamMember_userId_user_id_fk", + "tableFrom": "teamMember", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_site_access": { + "name": "team_site_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "site_id": { + "name": "site_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "team_site_access_team_idx": { + "name": "team_site_access_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_site_access_site_idx": { + "name": "team_site_access_site_idx", + "columns": [ + { + "expression": "site_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_site_access_team_id_team_id_fk": { + "name": "team_site_access_team_id_team_id_fk", + "tableFrom": "team_site_access", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_site_access_site_id_sites_site_id_fk": { + "name": "team_site_access_site_id_sites_site_id_fk", + "tableFrom": "team_site_access", + "tableTo": "sites", + "columnsFrom": [ + "site_id" + ], + "columnsTo": [ + "site_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "team_site_access_unique": { + "name": "team_site_access_unique", + "nullsNotDistinct": false, + "columns": [ + "team_id", + "site_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.telemetry": { + "name": "telemetry", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "table_counts": { + "name": "table_counts", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "clickhouse_size_gb": { + "name": "clickhouse_size_gb", + "type": "real", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.uptime_alert_history": { + "name": "uptime_alert_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "alert_id": { + "name": "alert_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "triggered_at": { + "name": "triggered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "alert_data": { + "name": "alert_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "uptime_alert_history_alert_id_uptime_alerts_id_fk": { + "name": "uptime_alert_history_alert_id_uptime_alerts_id_fk", + "tableFrom": "uptime_alert_history", + "tableTo": "uptime_alerts", + "columnsFrom": [ + "alert_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "uptime_alert_history_monitor_id_uptime_monitors_id_fk": { + "name": "uptime_alert_history_monitor_id_uptime_monitors_id_fk", + "tableFrom": "uptime_alert_history", + "tableTo": "uptime_monitors", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.uptime_alerts": { + "name": "uptime_alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "alert_type": { + "name": "alert_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "alert_config": { + "name": "alert_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "conditions": { + "name": "conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "uptime_alerts_monitor_id_uptime_monitors_id_fk": { + "name": "uptime_alerts_monitor_id_uptime_monitors_id_fk", + "tableFrom": "uptime_alerts", + "tableTo": "uptime_monitors", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.uptime_incidents": { + "name": "uptime_incidents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start_time": { + "name": "start_time", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "end_time": { + "name": "end_time", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "acknowledged_by": { + "name": "acknowledged_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acknowledged_at": { + "name": "acknowledged_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_type": { + "name": "last_error_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_count": { + "name": "failure_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "uptime_incidents_organization_id_organization_id_fk": { + "name": "uptime_incidents_organization_id_organization_id_fk", + "tableFrom": "uptime_incidents", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "uptime_incidents_monitor_id_uptime_monitors_id_fk": { + "name": "uptime_incidents_monitor_id_uptime_monitors_id_fk", + "tableFrom": "uptime_incidents", + "tableTo": "uptime_monitors", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "uptime_incidents_acknowledged_by_user_id_fk": { + "name": "uptime_incidents_acknowledged_by_user_id_fk", + "tableFrom": "uptime_incidents", + "tableTo": "user", + "columnsFrom": [ + "acknowledged_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "uptime_incidents_resolved_by_user_id_fk": { + "name": "uptime_incidents_resolved_by_user_id_fk", + "tableFrom": "uptime_incidents", + "tableTo": "user", + "columnsFrom": [ + "resolved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.uptime_monitor_status": { + "name": "uptime_monitor_status", + "schema": "", + "columns": { + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_check_at": { + "name": "next_check_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "current_status": { + "name": "current_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'unknown'" + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "consecutive_successes": { + "name": "consecutive_successes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "uptime_percentage_24h": { + "name": "uptime_percentage_24h", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "uptime_percentage_7d": { + "name": "uptime_percentage_7d", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "uptime_percentage_30d": { + "name": "uptime_percentage_30d", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "average_response_time_24h": { + "name": "average_response_time_24h", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "uptime_monitor_status_updated_at_idx": { + "name": "uptime_monitor_status_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "uptime_monitor_status_monitor_id_uptime_monitors_id_fk": { + "name": "uptime_monitor_status_monitor_id_uptime_monitors_id_fk", + "tableFrom": "uptime_monitor_status", + "tableTo": "uptime_monitors", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "uptime_monitor_status_current_status_check": { + "name": "uptime_monitor_status_current_status_check", + "value": "current_status IN ('up', 'down', 'unknown')" + }, + "uptime_monitor_status_uptime_24h_check": { + "name": "uptime_monitor_status_uptime_24h_check", + "value": "uptime_percentage_24h >= 0 AND uptime_percentage_24h <= 100" + }, + "uptime_monitor_status_uptime_7d_check": { + "name": "uptime_monitor_status_uptime_7d_check", + "value": "uptime_percentage_7d >= 0 AND uptime_percentage_7d <= 100" + }, + "uptime_monitor_status_uptime_30d_check": { + "name": "uptime_monitor_status_uptime_30d_check", + "value": "uptime_percentage_30d >= 0 AND uptime_percentage_30d <= 100" + } + }, + "isRLSEnabled": false + }, + "public.uptime_monitors": { + "name": "uptime_monitors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "monitor_type": { + "name": "monitor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "http_config": { + "name": "http_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tcp_config": { + "name": "tcp_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "validation_rules": { + "name": "validation_rules", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "monitoring_type": { + "name": "monitoring_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'local'" + }, + "selected_regions": { + "name": "selected_regions", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[\"local\"]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "uptime_monitors_organization_id_organization_id_fk": { + "name": "uptime_monitors_organization_id_organization_id_fk", + "tableFrom": "uptime_monitors", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "uptime_monitors_created_by_user_id_fk": { + "name": "uptime_monitors_created_by_user_id_fk", + "tableFrom": "uptime_monitors", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "displayUsername": { + "name": "displayUsername", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "banReason": { + "name": "banReason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banExpires": { + "name": "banExpires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "stripeCustomerId": { + "name": "stripeCustomerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "overMonthlyLimit": { + "name": "overMonthlyLimit", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "monthlyEventCount": { + "name": "monthlyEventCount", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sendAutoEmailReports": { + "name": "sendAutoEmailReports", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "scheduled_tip_email_ids": { + "name": "scheduled_tip_email_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_username_unique": { + "name": "user_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + }, + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_aliases": { + "name": "user_aliases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "site_id": { + "name": "site_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "anonymous_id": { + "name": "anonymous_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_aliases_user_idx": { + "name": "user_aliases_user_idx", + "columns": [ + { + "expression": "site_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_aliases_anon_idx": { + "name": "user_aliases_anon_idx", + "columns": [ + { + "expression": "site_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "anonymous_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_aliases_site_id_sites_site_id_fk": { + "name": "user_aliases_site_id_sites_site_id_fk", + "tableFrom": "user_aliases", + "tableTo": "sites", + "columnsFrom": [ + "site_id" + ], + "columnsTo": [ + "site_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_aliases_site_anon_unique": { + "name": "user_aliases_site_anon_unique", + "nullsNotDistinct": false, + "columns": [ + "site_id", + "anonymous_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_profiles": { + "name": "user_profiles", + "schema": "", + "columns": { + "site_id": { + "name": "site_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "traits": { + "name": "traits", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_profiles_site_idx": { + "name": "user_profiles_site_idx", + "columns": [ + { + "expression": "site_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_profiles_site_id_sites_site_id_fk": { + "name": "user_profiles_site_id_sites_site_id_fk", + "tableFrom": "user_profiles", + "tableTo": "sites", + "columnsFrom": [ + "site_id" + ], + "columnsTo": [ + "site_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_profiles_site_id_user_id_pk": { + "name": "user_profiles_site_id_user_id_pk", + "columns": [ + "site_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.import_platform_enum": { + "name": "import_platform_enum", + "schema": "public", + "values": [ + "umami", + "simple_analytics", + "plausible" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/server/drizzle/meta/_journal.json b/server/drizzle/meta/_journal.json index a959fb25c..c898ddd1b 100644 --- a/server/drizzle/meta/_journal.json +++ b/server/drizzle/meta/_journal.json @@ -92,6 +92,13 @@ "when": 1784511287442, "tag": "0012_perfect_nebula", "breakpoints": true + }, + { + "idx": 13, + "version": "7", + "when": 1784532118944, + "tag": "0013_dizzy_celestials", + "breakpoints": true } ] } \ No newline at end of file diff --git a/server/public/script-full.js b/server/public/script-full.js index b85f78721..c1d9f5768 100644 --- a/server/public/script-full.js +++ b/server/public/script-full.js @@ -88,6 +88,24 @@ return createVisitorId(); } } + function getOrCreatePersistentClientId(namespace) { + const key = `${namespace}-persistent-id`; + try { + const stored = localStorage.getItem(key); + if (stored) return stored; + const id = createVisitorId(); + localStorage.setItem(key, id); + return id; + } catch (e2) { + return void 0; + } + } + function clearPersistentClientId(namespace) { + try { + localStorage.removeItem(`${namespace}-persistent-id`); + } catch (e2) { + } + } function getIdentifiedUserId(namespace) { try { return localStorage.getItem(`${namespace}-user-id`) || void 0; @@ -238,6 +256,11 @@ trackCopy: apiConfig.trackCopy ?? defaultConfig.trackCopy, trackFormInteractions: apiConfig.trackFormInteractions ?? defaultConfig.trackFormInteractions }; + if (apiConfig.persistentClientIds) { + resolvedConfig.persistentClientId = getOrCreatePersistentClientId(namespace); + } else { + clearPersistentClientId(namespace); + } } else { console.warn("Failed to fetch tracking config from API, using defaults"); } @@ -419,6 +442,7 @@ this.eventBuffer = []; const batch = { userId: this.userId, + ...this.config.persistentClientId && { anonymousId: this.config.persistentClientId }, events, metadata: { pageUrl: window.location.href, @@ -729,6 +753,9 @@ if (this.customUserId) { payload.user_id = this.customUserId; } + if (this.config.persistentClientId) { + payload.anonymous_id = this.config.persistentClientId; + } if (this.config.tag) { payload.tag = this.config.tag; } diff --git a/server/public/script.js b/server/public/script.js index a0d815f6c..6bb8194b7 100644 --- a/server/public/script.js +++ b/server/public/script.js @@ -1 +1 @@ -"use strict";(()=>{var De=Object.defineProperty;var Ue=(n,e,t)=>e in n?De(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var R=(n,e,t)=>Ue(n,typeof e!="symbol"?e+"":e,t);function He(n){if(n.startsWith("re:")){let l=n.slice(3);if(!l)throw new Error("Empty regex pattern");return new RegExp(l)}let t="__DOUBLE_ASTERISK_TOKEN__",r="__SINGLE_ASTERISK_TOKEN__",s=n.replace(/\*\*/g,t).replace(/\*/g,r).replace(/[.+?^${}()|[\]\\]/g,"\\$&");s=s.replace(new RegExp(`/${t}/`,"g"),"/(?:.+/)?"),s=s.replace(new RegExp(t,"g"),".*"),s=s.replace(/\//g,"\\/");let a=s.replace(new RegExp(r,"g"),"[^/]+");return new RegExp("^"+a+"$")}function $(n,e){for(let t of e)try{if(He(t).test(n))return t}catch(r){console.error(`Invalid pattern: ${t}`,r)}return null}function ce(n,e){let t=null;return(...r)=>{t&&clearTimeout(t),t=setTimeout(()=>n(...r),e)}}function le(n){try{let e=window.location.hostname,t=new URL(n).hostname;return t!==e&&t!==""}catch{return!1}}function T(n,e){if(!n)return e;try{let t=JSON.parse(n);return Array.isArray(e)&&!Array.isArray(t)?e:t}catch(t){return console.error("Error parsing JSON:",t),e}}function ue(){try{if(crypto?.randomUUID)return crypto.randomUUID()}catch{}return`${Date.now().toString(36)}-${Math.random().toString(36).slice(2,12)}`}function We(n){let e=`${n}-visitor-id`;try{let t=localStorage.getItem(e);if(t)return t;let r=ue();return localStorage.setItem(e,r),r}catch{return ue()}}function Ve(n){try{return localStorage.getItem(`${n}-user-id`)||void 0}catch{return}}function $e(n){return n.hash&&n.hash.startsWith("#/")?n.hash.substring(1):n.pathname}async function ze(n,e,t,r){try{let i=new URL(window.location.href),s=await fetch(`${n}/site/${e}/feature-flags/evaluate`,{method:"POST",headers:{"Content-Type":"application/json"},credentials:"omit",body:JSON.stringify({anonymousId:r,identifiedUserId:Ve(t),hostname:i.hostname,pathname:$e(i),querystring:i.search,query:Object.fromEntries(i.searchParams.entries()),referrer:document.referrer,language:navigator.language,screenWidth:screen.width,screenHeight:screen.height})});if(!s.ok)return{};let a=await s.json();return a?.flags&&typeof a.flags=="object"?a.flags:{}}catch{return{}}}async function de(n){let e=n.getAttribute("src");if(!e)return console.error("Script src attribute is missing"),null;let t=e.split("/script.js")[0];if(!t)return console.error("Please provide a valid analytics host"),null;let r=n.getAttribute("data-site-id")||n.getAttribute("site-id");if(!r)return console.error("Please provide a valid site ID using the data-site-id attribute"),null;let i=n.getAttribute("data-namespace")||"rybbit",s=We(i),a=T(n.getAttribute("data-skip-patterns"),[]),l=T(n.getAttribute("data-mask-patterns"),[]),d=T(n.getAttribute("data-replay-mask-text-selectors"),[]),u=n.getAttribute("data-debounce")?Math.max(0,parseInt(n.getAttribute("data-debounce"))):500,g=n.getAttribute("data-replay-batch-size")?Math.max(1,parseInt(n.getAttribute("data-replay-batch-size"))):250,h=n.getAttribute("data-replay-batch-interval")?Math.max(1e3,parseInt(n.getAttribute("data-replay-batch-interval"))):5e3,C=n.getAttribute("data-replay-block-class")||void 0,m=n.getAttribute("data-replay-block-selector")||void 0,o=n.getAttribute("data-replay-ignore-class")||void 0,c=n.getAttribute("data-replay-ignore-selector")||void 0,p=n.getAttribute("data-replay-mask-text-class")||void 0,w=n.getAttribute("data-replay-mask-all-inputs"),E=w!==null?w!=="false":void 0,S=n.getAttribute("data-replay-mask-input-options"),F=S?T(S,{password:!0,email:!0}):void 0,ne=n.getAttribute("data-replay-collect-fonts"),Me=ne!==null?ne!=="false":void 0,ie=n.getAttribute("data-replay-sampling"),xe=ie?T(ie,{}):void 0,se=n.getAttribute("data-replay-slim-dom-options"),Be=se?T(se,{}):void 0,ae=n.getAttribute("data-replay-sample-rate"),Oe=ae?Math.min(100,Math.max(0,parseInt(ae,10))):void 0,Ne=n.getAttribute("data-tag")||"",f={namespace:i,analyticsHost:t,siteId:r,visitorId:s,debounceDuration:u,sessionReplayBatchSize:g,sessionReplayBatchInterval:h,sessionReplayMaskTextSelectors:d,skipPatterns:a,maskPatterns:l,autoTrackPageview:!0,autoTrackSpa:!0,trackQuerystring:!0,trackOutbound:!0,enableWebVitals:!1,trackErrors:!1,enableSessionReplay:!1,trackButtonClicks:!1,trackCopy:!1,trackFormInteractions:!1,tag:Ne,featureFlags:{},sessionReplayBlockClass:C,sessionReplayBlockSelector:m,sessionReplayIgnoreClass:o,sessionReplayIgnoreSelector:c,sessionReplayMaskTextClass:p,sessionReplayMaskAllInputs:E,sessionReplayMaskInputOptions:F,sessionReplayCollectFonts:Me,sessionReplaySampling:xe,sessionReplaySlimDOMOptions:Be,sessionReplaySampleRate:Oe},W=f;try{let V=`${t}/site/tracking-config/${r}`,oe=await fetch(V,{method:"GET",credentials:"omit"});if(oe.ok){let y=await oe.json();W={...f,autoTrackPageview:y.trackInitialPageView??f.autoTrackPageview,autoTrackSpa:y.trackSpaNavigation??f.autoTrackSpa,trackQuerystring:y.trackUrlParams??f.trackQuerystring,trackOutbound:y.trackOutbound??f.trackOutbound,enableWebVitals:y.webVitals??f.enableWebVitals,trackErrors:y.trackErrors??f.trackErrors,enableSessionReplay:y.sessionReplay??f.enableSessionReplay,trackButtonClicks:y.trackButtonClicks??f.trackButtonClicks,trackCopy:y.trackCopy??f.trackCopy,trackFormInteractions:y.trackFormInteractions??f.trackFormInteractions}}else console.warn("Failed to fetch tracking config from API, using defaults")}catch(V){console.warn("Error fetching tracking config:",V)}return W.featureFlags=await ze(t,r,i,s),W}var pe="rybbit-replay-sampled";function je(n){if(n>=100)return!0;if(n<=0)return!1;try{let e=sessionStorage.getItem(pe);if(e!==null)return e==="1";let t=Math.random()*100{let r=document.createElement("script");r.src=`${this.config.analyticsHost}/replay.js`,r.async=!1,r.onload=()=>{e()},r.onerror=()=>t(new Error("Failed to load rrweb")),document.head.appendChild(r)})}startRecording(){if(!(this.isRecording||!window.rrweb||!this.config.enableSessionReplay))try{let e={mousemove:!1,mouseInteraction:{MouseUp:!1,MouseDown:!1,Click:!0,ContextMenu:!1,DblClick:!0,Focus:!0,Blur:!0,TouchStart:!1,TouchEnd:!1},scroll:500,input:"last",media:800},t={script:!1,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaDescKeywords:!0,headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaAuthorship:!0,headMetaVerification:!0},r={emit:i=>{this.addEvent({type:i.type,data:i.data,timestamp:i.timestamp||Date.now()})},recordCanvas:!1,checkoutEveryNms:6e4,checkoutEveryNth:500,blockClass:this.config.sessionReplayBlockClass??"rr-block",blockSelector:this.config.sessionReplayBlockSelector??null,ignoreClass:this.config.sessionReplayIgnoreClass??"rr-ignore",ignoreSelector:this.config.sessionReplayIgnoreSelector??null,maskTextClass:this.config.sessionReplayMaskTextClass??"rr-mask",maskAllInputs:this.config.sessionReplayMaskAllInputs??!0,maskInputOptions:this.config.sessionReplayMaskInputOptions??{password:!0,email:!0},collectFonts:this.config.sessionReplayCollectFonts??!0,sampling:this.config.sessionReplaySampling??e,slimDOMOptions:this.config.sessionReplaySlimDOMOptions??t};this.config.sessionReplayMaskTextSelectors&&this.config.sessionReplayMaskTextSelectors.length>0&&(r.maskTextSelector=this.config.sessionReplayMaskTextSelectors.join(", ")),this.stopRecordingFn=window.rrweb.record(r),this.isRecording=!0,this.setupBatchTimer()}catch{}}stopRecording(){this.isRecording&&(this.stopRecordingFn&&this.stopRecordingFn(),this.isRecording=!1,this.clearBatchTimer(),this.eventBuffer.length>0&&this.flushEvents())}isActive(){return this.isRecording}addEvent(e){this.eventBuffer.push(e),this.eventBuffer.length>=this.config.sessionReplayBatchSize&&this.flushEvents()}setupBatchTimer(){this.clearBatchTimer(),this.batchTimer=window.setInterval(()=>{this.eventBuffer.length>0&&this.flushEvents()},this.config.sessionReplayBatchInterval)}clearBatchTimer(){this.batchTimer&&(clearInterval(this.batchTimer),this.batchTimer=void 0)}async flushEvents(){if(this.eventBuffer.length===0)return;let e=[...this.eventBuffer];this.eventBuffer=[];let t={userId:this.userId,events:e,metadata:{pageUrl:window.location.href,viewportWidth:screen.width,viewportHeight:screen.height,language:navigator.language}};try{await this.sendBatch(t)}catch{this.eventBuffer.unshift(...e)}}updateUserId(e){e!==this.userId&&(this.eventBuffer.length>0&&this.flushEvents(),this.userId=e)}onPageChange(){this.isRecording&&this.flushEvents()}cleanup(){this.stopRecording()}};var b={automationApi:1,webdriver:1,zeroOuterDimensions:2,missingChrome:4,swiftShader:8,emptyPlugins:16,defaultViewport800x600:32,defaultViewport1024x768:64,impossibleDimensions:128,outerDimensionsWeird:256,pluginApiAbsence:512},ge=null,Ge=10;function he(){return me().score}function fe(){return me().mask}function me(){return ge??(ge=Ke()),ge}function Ke(){let n=0,e=0;function t(r,i){(e&r)===0&&(e|=r,n+=i)}try{let r=navigator.userAgent,i=/Chrome\//.test(r)&&!/\bwv\b|; wv\)/.test(r),s=/Windows NT|Macintosh|X11|Linux x86_64/.test(r)&&!/Mobile|Android|iPhone|iPad/.test(r),a=Number(window.screen?.width),l=Number(window.screen?.height),d=Number(window.outerWidth),u=Number(window.outerHeight),g=Number(window.innerWidth),h=Number(window.innerHeight),m=["__webdriver_evaluate","__selenium_evaluate","__webdriver_script_function","__webdriver_script_func","__webdriver_script_fn","__fxdriver_evaluate","__driver_unwrapped","__webdriver_unwrapped","__driver_evaluate","__selenium_unwrapped","__fxdriver_unwrapped","_phantom","callPhantom","__nightmare","domAutomation","domAutomationController"].some(c=>c in window||c in document);(navigator.webdriver===!0||m)&&t(b.automationApi,3),(u===0||d===0)&&t(b.zeroOuterDimensions,2),(!Number.isFinite(a)||!Number.isFinite(l)||a<=0||l<=0||a>1e5||l>1e5)&&t(b.impossibleDimensions,3),s&&a===800&&l===600&&t(b.defaultViewport800x600,3),s&&a===1024&&l===768&&t(b.defaultViewport1024x768,3),Number.isFinite(d)&&Number.isFinite(u)&&Number.isFinite(g)&&Number.isFinite(h)&&d>0&&u>0&&g>0&&h>0&&(d+8this.sendSessionReplayBatch(e)),await this.sessionReplayRecorder.initialize()}catch(e){console.error("Failed to initialize session replay:",e)}}async sendSessionReplayBatch(e){try{await fetch(`${this.config.analyticsHost}/session-replay/record/${this.config.siteId}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e),mode:"cors",keepalive:!1})}catch(t){throw console.error("Failed to send session replay batch:",t),t}}createBasePayload(){let e=new URL(window.location.href),t=e.pathname;if(e.hash&&e.hash.startsWith("#/")&&(t=e.hash.substring(1)),$(t,this.config.skipPatterns))return null;let r=$(t,this.config.maskPatterns);r&&(t=r);let i={site_id:this.config.siteId,hostname:e.hostname,pathname:t,querystring:this.config.trackQuerystring?e.search:"",screenWidth:screen.width,screenHeight:screen.height,language:navigator.language,page_title:document.title,referrer:document.referrer,_bs:he(),_bsm:fe()};this.customUserId&&(i.user_id=this.customUserId),this.config.tag&&(i.tag=this.config.tag);let s=this.getFeatureFlagEventPayload();return Object.keys(s).length>0&&(i.feature_flags=s),i}async sendTrackingData(e){try{await fetch(`${this.config.analyticsHost}/track`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e),mode:"cors",keepalive:!0})}catch(t){console.error("Failed to send tracking data:",t)}}track(e,t="",r={}){if(e==="custom_event"&&(!t||typeof t!="string")){console.error("Event name is required and must be a string for custom events");return}let i=this.createBasePayload();if(!i)return;let a={...i,type:e,event_name:t,properties:["custom_event","outbound","error","button_click","copy","form_submit","input_change"].includes(e)?JSON.stringify(r):void 0};this.sendTrackingData(a)}trackPageview(){this.track("pageview")}trackEvent(e,t={}){this.track("custom_event",e,t)}getFeatureFlag(e,t){let r=this.config.featureFlags?.[e];if(!r)return t;let i=`${e}:${r.version}:${this.serializeFeatureFlagValue(r.value)}`;return this.exposedFeatureFlags.has(i)||(this.exposedFeatureFlags.add(i),this.trackEvent("feature_flag_exposure",{key:e,value:this.serializeFeatureFlagValue(r.value),version:r.version,reason:r.reason})),r.value}getFeatureFlags(){return Object.fromEntries(Object.entries(this.config.featureFlags||{}).map(([e,t])=>[e,t.value]))}getFeatureFlagPayload(e,t){let r=this.config.featureFlags?.[e];return!r||r.payload===void 0?t:r.payload}getFeatureFlagPayloads(){return Object.fromEntries(Object.entries(this.config.featureFlags||{}).filter(([,e])=>e.payload!==void 0).map(([e,t])=>[e,t.payload]))}trackOutbound(e,t="",r="_self"){this.track("outbound","",{url:e,text:t,target:r})}trackWebVitals(e){let t=this.createBasePayload();if(!t)return;let r={...t,type:"performance",event_name:"web-vitals",...e};this.sendTrackingData(r)}trackError(e,t={}){let r=e?.message||"";if(r.includes("ResizeObserver loop completed with undelivered notifications")||r.includes("ResizeObserver loop limit exceeded"))return;let i=window.location.origin,s=t.filename||"",a=e.stack||"";if(s)try{if(new URL(s).origin!==i)return}catch{}else if(a&&!a.includes(i))return;let d=[e.name||"Error",r,t.filename||"",t.lineno??"",t.colno??""].join("|"),u=Date.now(),g=6e4,h=this.errorDedupeCache.get(d);if(h&&u-hg){for(let[o,c]of this.errorDedupeCache.entries())u-c>C&&this.errorDedupeCache.delete(o);this.errorDedupeLastCleanup=u}let m={message:e.message?.substring(0,500)||"Unknown error",stack:a.substring(0,2e3)||""};if(s&&(m.fileName=s),t.lineno){let o=typeof t.lineno=="string"?parseInt(t.lineno,10):t.lineno;o&&o!==0&&(m.lineNumber=o)}if(t.colno){let o=typeof t.colno=="string"?parseInt(t.colno,10):t.colno;o&&o!==0&&(m.columnNumber=o)}for(let o in t)!["lineno","colno"].includes(o)&&t[o]!==void 0&&(m[o]=t[o]);this.track("error",e.name||"Error",m)}trackButtonClick(e){this.track("button_click","",e)}trackCopy(e){this.track("copy","",e)}trackFormSubmit(e){this.track("form_submit","",e)}trackInputChange(e){this.track("input_change","",e)}identify(e,t){if(typeof e!="string"||e.trim()===""){console.error("User ID must be a non-empty string");return}this.customUserId=e.trim();try{localStorage.setItem(`${this.config.namespace}-user-id`,this.customUserId)}catch{console.warn("Could not persist user ID to localStorage")}this.sendIdentifyEvent(this.customUserId,t,!0).then(()=>this.refreshFeatureFlags()),this.sessionReplayRecorder&&this.sessionReplayRecorder.updateUserId(this.customUserId)}setTraits(e){if(!e||typeof e!="object"){console.error("Traits must be an object");return}let t=this.customUserId;if(!t){console.warn("Cannot set traits without identifying user first. Call identify() first.");return}this.sendIdentifyEvent(t,e,!1).then(()=>this.refreshFeatureFlags())}async sendIdentifyEvent(e,t,r=!0){try{await fetch(`${this.config.analyticsHost}/identify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({site_id:this.config.siteId,user_id:e,traits:t,is_new_identify:r}),mode:"cors",keepalive:!0})}catch(i){console.error("Failed to send identify event:",i)}}clearUserId(){this.customUserId=null;try{localStorage.removeItem(`${this.config.namespace}-user-id`)}catch{}this.sessionReplayRecorder&&this.sessionReplayRecorder.updateUserId(""),this.refreshFeatureFlags()}getUserId(){return this.customUserId}startSessionReplay(){this.sessionReplayRecorder?this.sessionReplayRecorder.startRecording():console.warn("Session replay not initialized")}stopSessionReplay(){this.sessionReplayRecorder&&this.sessionReplayRecorder.stopRecording()}isSessionReplayActive(){return this.sessionReplayRecorder?.isActive()??!1}onPageChange(){this.refreshFeatureFlags(),this.sessionReplayRecorder&&this.sessionReplayRecorder.onPageChange()}cleanup(){this.sessionReplayRecorder&&this.sessionReplayRecorder.cleanup()}};var Te=-1,I=n=>{addEventListener("pageshow",(e=>{e.persisted&&(Te=e.timeStamp,n(e))}),!0)},v=(n,e,t,r)=>{let i,s;return a=>{e.value>=0&&(a||r)&&(s=e.value-(i??0),(s||i===void 0)&&(i=e.value,e.delta=s,e.rating=((l,d)=>l>d[1]?"poor":l>d[0]?"needs-improvement":"good")(e.value,t),n(e)))}},Y=n=>{requestAnimationFrame((()=>requestAnimationFrame((()=>n()))))},Z=()=>{let n=performance.getEntriesByType("navigation")[0];if(n&&n.responseStart>0&&n.responseStartZ()?.activationStart??0,k=(n,e=-1)=>{let t=Z(),r="navigate";return Te>=0?r="back-forward-cache":t&&(document.prerendering||_()>0?r="prerender":document.wasDiscarded?r="restore":t.type&&(r=t.type.replace(/_/g,"-"))),{name:n,value:e,rating:"good",delta:0,entries:[],id:`v5-${Date.now()}-${Math.floor(8999999999999*Math.random())+1e12}`,navigationType:r}},z=new WeakMap;function ee(n,e){return z.get(n)||z.set(n,new e),z.get(n)}var G=class{constructor(){R(this,"t");R(this,"i",0);R(this,"o",[])}h(e){if(e.hadRecentInput)return;let t=this.o[0],r=this.o.at(-1);this.i&&t&&r&&e.startTime-r.startTime<1e3&&e.startTime-t.startTime<5e3?(this.i+=e.value,this.o.push(e)):(this.i=e.value,this.o=[e]),this.t?.(e)}},A=(n,e,t={})=>{try{if(PerformanceObserver.supportedEntryTypes.includes(n)){let r=new PerformanceObserver((i=>{Promise.resolve().then((()=>{e(i.getEntries())}))}));return r.observe({type:n,buffered:!0,...t}),r}}catch{}},te=n=>{let e=!1;return()=>{e||(n(),e=!0)}},P=-1,Ce=new Set,ye=()=>document.visibilityState!=="hidden"||document.prerendering?1/0:0,K=n=>{if(document.visibilityState==="hidden"){if(n.type==="visibilitychange")for(let e of Ce)e();isFinite(P)||(P=n.type==="visibilitychange"?n.timeStamp:0,removeEventListener("prerenderingchange",K,!0))}},B=()=>{if(P<0){let n=_();P=(document.prerendering?void 0:globalThis.performance.getEntriesByType("visibility-state").filter((t=>t.name==="hidden"&&t.startTime>n))[0]?.startTime)??ye(),addEventListener("visibilitychange",K,!0),addEventListener("prerenderingchange",K,!0),I((()=>{setTimeout((()=>{P=ye()}))}))}return{get firstHiddenTime(){return P},onHidden(n){Ce.add(n)}}},O=n=>{document.prerendering?addEventListener("prerenderingchange",(()=>n()),!0):n()},be=[1800,3e3],re=(n,e={})=>{O((()=>{let t=B(),r,i=k("FCP"),s=A("paint",(a=>{for(let l of a)l.name==="first-contentful-paint"&&(s.disconnect(),l.startTime{i=k("FCP"),r=v(n,i,be,e.reportAllChanges),Y((()=>{i.value=performance.now()-a.timeStamp,r(!0)}))})))}))},ve=[.1,.25],Pe=(n,e={})=>{let t=B();re(te((()=>{let r,i=k("CLS",0),s=ee(e,G),a=d=>{for(let u of d)s.h(u);s.i>i.value&&(i.value=s.i,i.entries=s.o,r())},l=A("layout-shift",a);l&&(r=v(n,i,ve,e.reportAllChanges),t.onHidden((()=>{a(l.takeRecords()),r(!0)})),I((()=>{s.i=0,i=k("CLS",0),r=v(n,i,ve,e.reportAllChanges),Y((()=>r()))})),setTimeout(r))})))},Ie=0,j=1/0,x=0,qe=n=>{for(let e of n)e.interactionId&&(j=Math.min(j,e.interactionId),x=Math.max(x,e.interactionId),Ie=x?(x-j)/7+1:0)},J,ke=()=>J?Ie:performance.interactionCount??0,Xe=()=>{"interactionCount"in performance||J||(J=A("event",qe,{type:"event",buffered:!0,durationThreshold:0}))},we=0,q=class{constructor(){R(this,"u",[]);R(this,"l",new Map);R(this,"m");R(this,"p")}v(){we=ke(),this.u.length=0,this.l.clear()}L(){let e=Math.min(this.u.length-1,Math.floor((ke()-we)/50));return this.u[e]}h(e){if(this.m?.(e),!e.interactionId&&e.entryType!=="first-input")return;let t=this.u.at(-1),r=this.l.get(e.interactionId);if(r||this.u.length<10||e.duration>t.P){if(r?e.duration>r.P?(r.entries=[e],r.P=e.duration):e.duration===r.P&&e.startTime===r.entries[0].startTime&&r.entries.push(e):(r={id:e.interactionId,entries:[e],P:e.duration},this.l.set(r.id,r),this.u.push(r)),this.u.sort(((i,s)=>s.P-i.P)),this.u.length>10){let i=this.u.splice(10);for(let s of i)this.l.delete(s.id)}this.p?.(r)}}},_e=n=>{let e=globalThis.requestIdleCallback||setTimeout;document.visibilityState==="hidden"?n():(n=te(n),addEventListener("visibilitychange",n,{once:!0,capture:!0}),e((()=>{n(),removeEventListener("visibilitychange",n,{capture:!0})})))},Re=[200,500],Ae=(n,e={})=>{if(!globalThis.PerformanceEventTiming||!("interactionId"in PerformanceEventTiming.prototype))return;let t=B();O((()=>{Xe();let r,i=k("INP"),s=ee(e,q),a=d=>{_e((()=>{for(let g of d)s.h(g);let u=s.L();u&&u.P!==i.value&&(i.value=u.P,i.entries=u.entries,r())}))},l=A("event",a,{durationThreshold:e.durationThreshold??40});r=v(n,i,Re,e.reportAllChanges),l&&(l.observe({type:"first-input",buffered:!0}),t.onHidden((()=>{a(l.takeRecords()),r(!0)})),I((()=>{s.v(),i=k("INP"),r=v(n,i,Re,e.reportAllChanges)})))}))},X=class{constructor(){R(this,"m")}h(e){this.m?.(e)}},Ee=[2500,4e3],Fe=(n,e={})=>{O((()=>{let t=B(),r,i=k("LCP"),s=ee(e,X),a=d=>{e.reportAllChanges||(d=d.slice(-1));for(let u of d)s.h(u),u.startTime{a(l.takeRecords()),l.disconnect(),r(!0)})),u=g=>{g.isTrusted&&(_e(d),removeEventListener(g.type,u,{capture:!0}))};for(let g of["keydown","click","visibilitychange"])addEventListener(g,u,{capture:!0});I((g=>{i=k("LCP"),r=v(n,i,Ee,e.reportAllChanges),Y((()=>{i.value=performance.now()-g.timeStamp,r(!0)}))}))}}))},Se=[800,1800],Q=n=>{document.prerendering?O((()=>Q(n))):document.readyState!=="complete"?addEventListener("load",(()=>Q(n)),!0):setTimeout(n)},Le=(n,e={})=>{let t=k("TTFB"),r=v(n,t,Se,e.reportAllChanges);Q((()=>{let i=Z();i&&(t.value=Math.max(i.responseStart-_(),0),t.entries=[i],r(!0),I((()=>{t=k("TTFB",0),r=v(n,t,Se,e.reportAllChanges),r(!0)})))}))};var N=class{constructor(e){this.data={lcp:null,cls:null,inp:null,fcp:null,ttfb:null};this.sent=!1;this.timeout=null;this.onReadyCallback=null;this.onReadyCallback=e}initialize(){try{Fe(this.collectMetric.bind(this)),Pe(this.collectMetric.bind(this)),Ae(this.collectMetric.bind(this)),re(this.collectMetric.bind(this)),Le(this.collectMetric.bind(this)),this.timeout=setTimeout(()=>{this.sent||this.sendData()},2e4),window.addEventListener("beforeunload",()=>{this.sent||this.sendData()})}catch(e){console.warn("Error initializing web vitals tracking:",e)}}collectMetric(e){if(this.sent)return;let t=e.name.toLowerCase();this.data[t]=e.value,Object.values(this.data).every(i=>i!==null)&&this.sendData()}sendData(){this.sent||(this.sent=!0,this.timeout&&(clearTimeout(this.timeout),this.timeout=null),this.onReadyCallback&&this.onReadyCallback(this.data))}getData(){return{...this.data}}};var Qe=1e3,D=class{constructor(e,t){this.lastClickAt=new WeakMap;this.tracker=e,this.config=t}initialize(){document.addEventListener("click",this.handleClick.bind(this),!0)}handleClick(e){let t=e.target;this.config.trackButtonClicks&&this.isButton(t)&&this.trackButtonClick(t)}isButton(e){if(e.tagName==="BUTTON"||e.getAttribute("role")==="button")return!0;if(e.tagName==="INPUT"){let i=e.type?.toLowerCase();if(i==="submit"||i==="button")return!0}let t=e.parentElement,r=0;for(;t&&r<3;){if(t.tagName==="BUTTON"||t.getAttribute("role")==="button")return!0;t=t.parentElement,r++}return!1}trackButtonClick(e){let t=this.findButton(e);if(!t||t.hasAttribute("data-rybbit-event"))return;let r=Date.now(),i=this.lastClickAt.get(t);if(i!==void 0&&r-i500&&{textLength:r},sourceElement:s.tagName.toLowerCase()};this.tracker.trackCopy(a)}cleanup(){document.removeEventListener("copy",this.handleCopy.bind(this))}};var H=class{constructor(e,t){this.tracker=e,this.config=t,this.boundHandleSubmit=this.handleSubmit.bind(this),this.boundHandleChange=this.handleChange.bind(this)}initialize(){document.addEventListener("submit",this.boundHandleSubmit,!0),document.addEventListener("change",this.boundHandleChange,!0)}cleanup(){document.removeEventListener("submit",this.boundHandleSubmit,!0),document.removeEventListener("change",this.boundHandleChange,!0)}handleSubmit(e){let t=e.target;if(t.tagName!=="FORM")return;let r={formId:t.id||"",formName:t.name||"",formAction:t.action||"",method:(t.method||"get").toUpperCase(),fieldCount:t.elements.length,ariaLabel:t.getAttribute("aria-label")||void 0,...this.extractDataAttributes(t)};this.tracker.trackFormSubmit(r)}handleChange(e){let t=e.target,r=t.tagName.toUpperCase();if(!["INPUT","SELECT","TEXTAREA"].includes(r)||t.disabled||t.readOnly)return;if(r==="INPUT"){let a=t.type?.toLowerCase();if(a==="hidden"||a==="password")return}let i=t.name||t.id||t.getAttribute("aria-label")||t.placeholder||"",s={element:r.toLowerCase(),inputType:r==="INPUT"?t.type?.toLowerCase():void 0,inputName:i,formId:t.form?.id||void 0,formName:t.form?.name||void 0,...this.extractDataAttributes(t)};this.tracker.trackInputChange(s)}extractDataAttributes(e){let t={};for(let r of e.attributes)if(r.name.startsWith("data-rybbit-prop-")){let i=r.name.replace("data-rybbit-prop-","");t[i]=r.value}return t}};(async function(){let n=document.currentScript;if(!n){console.error("Could not find current script tag");return}let e=n.getAttribute("data-namespace")||"rybbit",t=`disable-${e}`;if(window.__RYBBIT_OPTOUT__||localStorage.getItem(t)!==null){window[e]={pageview:()=>{},event:()=>{},error:()=>{},trackOutbound:()=>{},identify:()=>{},setTraits:()=>{},clearUserId:()=>{},getUserId:()=>null,flag:(o,c)=>c,flagPayload:(o,c)=>c,flags:()=>({}),flagPayloads:()=>({}),onReady:()=>{},startSessionReplay:()=>{},stopSessionReplay:()=>{},isSessionReplayActive:()=>!1};return}let r=[],i=o=>(...c)=>{r.push([o,c])};window[e]={pageview:i("pageview"),event:i("event"),error:i("error"),trackOutbound:i("trackOutbound"),identify:i("identify"),setTraits:i("setTraits"),clearUserId:i("clearUserId"),getUserId:()=>null,flag:(o,c)=>c,flagPayload:(o,c)=>c,flags:()=>({}),flagPayloads:()=>({}),onReady:i("onReady"),startSessionReplay:i("startSessionReplay"),stopSessionReplay:i("stopSessionReplay"),isSessionReplayActive:()=>!1};let s=await de(n);if(!s)return;let a=new M(s);s.enableWebVitals&&new N(c=>{a.trackWebVitals(c)}).initialize();let l=null,d=null,u=null;s.trackButtonClicks&&(l=new D(a,s),l.initialize()),s.trackCopy&&(d=new U(a),d.initialize()),s.trackFormInteractions&&(u=new H(a,s),u.initialize()),s.trackErrors&&(window.addEventListener("error",o=>{a.trackError(o.error||new Error(o.message),{filename:o.filename,lineno:o.lineno,colno:o.colno})}),window.addEventListener("unhandledrejection",o=>{let c=o.reason instanceof Error?o.reason:new Error(String(o.reason));a.trackError(c,{type:"unhandledrejection"})}));let g=()=>a.trackPageview(),h=s.debounceDuration>0?ce(g,s.debounceDuration):g;function C(){if(document.addEventListener("click",function(o){let c=o.target;for(;c&&c!==document.documentElement;){if(c.hasAttribute("data-rybbit-event")){let p=c.getAttribute("data-rybbit-event");if(p){let w={};for(let E of c.attributes)if(E.name.startsWith("data-rybbit-prop-")){let S=E.name.replace("data-rybbit-prop-","");w[S]=E.value}a.trackEvent(p,w)}break}c=c.parentElement}if(s.trackOutbound){let p=o.target.closest("a");p?.href&&le(p.href)&&a.trackOutbound(p.href,p.innerText||p.textContent||"",p.target||"_self")}}),s.autoTrackSpa){let o=history.pushState,c=history.replaceState;history.pushState=function(...p){o.apply(this,p),h(),a.onPageChange()},history.replaceState=function(...p){c.apply(this,p),h(),a.onPageChange()},window.addEventListener("popstate",()=>{h(),a.onPageChange()}),window.addEventListener("hashchange",()=>{h(),a.onPageChange()})}}window[s.namespace]={pageview:()=>a.trackPageview(),event:(o,c={})=>a.trackEvent(o,c),error:(o,c={})=>a.trackError(o,c),trackOutbound:(o,c="",p="_self")=>a.trackOutbound(o,c,p),identify:(o,c)=>a.identify(o,c),setTraits:o=>a.setTraits(o),clearUserId:()=>a.clearUserId(),getUserId:()=>a.getUserId(),flag:(o,c)=>a.getFeatureFlag(o,c),flagPayload:(o,c)=>a.getFeatureFlagPayload(o,c),flags:()=>a.getFeatureFlags(),flagPayloads:()=>a.getFeatureFlagPayloads(),onReady:o=>o(window[s.namespace]),startSessionReplay:()=>a.startSessionReplay(),stopSessionReplay:()=>a.stopSessionReplay(),isSessionReplayActive:()=>a.isSessionReplayActive()};let m=window[s.namespace];for(let[o,c]of r)m[o](...c);C(),window.addEventListener("beforeunload",()=>{l?.cleanup(),d?.cleanup(),a.cleanup()}),s.autoTrackPageview&&a.trackPageview()})();})(); +"use strict";(()=>{var De=Object.defineProperty;var Ue=(n,e,t)=>e in n?De(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var R=(n,e,t)=>Ue(n,typeof e!="symbol"?e+"":e,t);function He(n){if(n.startsWith("re:")){let l=n.slice(3);if(!l)throw new Error("Empty regex pattern");return new RegExp(l)}let t="__DOUBLE_ASTERISK_TOKEN__",r="__SINGLE_ASTERISK_TOKEN__",s=n.replace(/\*\*/g,t).replace(/\*/g,r).replace(/[.+?^${}()|[\]\\]/g,"\\$&");s=s.replace(new RegExp(`/${t}/`,"g"),"/(?:.+/)?"),s=s.replace(new RegExp(t,"g"),".*"),s=s.replace(/\//g,"\\/");let a=s.replace(new RegExp(r,"g"),"[^/]+");return new RegExp("^"+a+"$")}function V(n,e){for(let t of e)try{if(He(t).test(n))return t}catch(r){console.error(`Invalid pattern: ${t}`,r)}return null}function le(n,e){let t=null;return(...r)=>{t&&clearTimeout(t),t=setTimeout(()=>n(...r),e)}}function ue(n){try{let e=window.location.hostname,t=new URL(n).hostname;return t!==e&&t!==""}catch{return!1}}function E(n,e){if(!n)return e;try{let t=JSON.parse(n);return Array.isArray(e)&&!Array.isArray(t)?e:t}catch(t){return console.error("Error parsing JSON:",t),e}}function z(){try{if(crypto?.randomUUID)return crypto.randomUUID()}catch{}return`${Date.now().toString(36)}-${Math.random().toString(36).slice(2,12)}`}function We(n){let e=`${n}-visitor-id`;try{let t=localStorage.getItem(e);if(t)return t;let r=z();return localStorage.setItem(e,r),r}catch{return z()}}function $e(n){let e=`${n}-persistent-id`;try{let t=localStorage.getItem(e);if(t)return t;let r=z();return localStorage.setItem(e,r),r}catch{return}}function Ve(n){try{localStorage.removeItem(`${n}-persistent-id`)}catch{}}function ze(n){try{return localStorage.getItem(`${n}-user-id`)||void 0}catch{return}}function je(n){return n.hash&&n.hash.startsWith("#/")?n.hash.substring(1):n.pathname}async function Ge(n,e,t,r){try{let i=new URL(window.location.href),s=await fetch(`${n}/site/${e}/feature-flags/evaluate`,{method:"POST",headers:{"Content-Type":"application/json"},credentials:"omit",body:JSON.stringify({anonymousId:r,identifiedUserId:ze(t),hostname:i.hostname,pathname:je(i),querystring:i.search,query:Object.fromEntries(i.searchParams.entries()),referrer:document.referrer,language:navigator.language,screenWidth:screen.width,screenHeight:screen.height})});if(!s.ok)return{};let a=await s.json();return a?.flags&&typeof a.flags=="object"?a.flags:{}}catch{return{}}}async function de(n){let e=n.getAttribute("src");if(!e)return console.error("Script src attribute is missing"),null;let t=e.split("/script.js")[0];if(!t)return console.error("Please provide a valid analytics host"),null;let r=n.getAttribute("data-site-id")||n.getAttribute("site-id");if(!r)return console.error("Please provide a valid site ID using the data-site-id attribute"),null;let i=n.getAttribute("data-namespace")||"rybbit",s=We(i),a=E(n.getAttribute("data-skip-patterns"),[]),l=E(n.getAttribute("data-mask-patterns"),[]),d=E(n.getAttribute("data-replay-mask-text-selectors"),[]),u=n.getAttribute("data-debounce")?Math.max(0,parseInt(n.getAttribute("data-debounce"))):500,g=n.getAttribute("data-replay-batch-size")?Math.max(1,parseInt(n.getAttribute("data-replay-batch-size"))):250,h=n.getAttribute("data-replay-batch-interval")?Math.max(1e3,parseInt(n.getAttribute("data-replay-batch-interval"))):5e3,T=n.getAttribute("data-replay-block-class")||void 0,m=n.getAttribute("data-replay-block-selector")||void 0,o=n.getAttribute("data-replay-ignore-class")||void 0,c=n.getAttribute("data-replay-ignore-selector")||void 0,p=n.getAttribute("data-replay-mask-text-class")||void 0,w=n.getAttribute("data-replay-mask-all-inputs"),C=w!==null?w!=="false":void 0,S=n.getAttribute("data-replay-mask-input-options"),F=S?E(S,{password:!0,email:!0}):void 0,ie=n.getAttribute("data-replay-collect-fonts"),Me=ie!==null?ie!=="false":void 0,se=n.getAttribute("data-replay-sampling"),xe=se?E(se,{}):void 0,ae=n.getAttribute("data-replay-slim-dom-options"),Be=ae?E(ae,{}):void 0,oe=n.getAttribute("data-replay-sample-rate"),Oe=oe?Math.min(100,Math.max(0,parseInt(oe,10))):void 0,Ne=n.getAttribute("data-tag")||"",f={namespace:i,analyticsHost:t,siteId:r,visitorId:s,debounceDuration:u,sessionReplayBatchSize:g,sessionReplayBatchInterval:h,sessionReplayMaskTextSelectors:d,skipPatterns:a,maskPatterns:l,autoTrackPageview:!0,autoTrackSpa:!0,trackQuerystring:!0,trackOutbound:!0,enableWebVitals:!1,trackErrors:!1,enableSessionReplay:!1,trackButtonClicks:!1,trackCopy:!1,trackFormInteractions:!1,tag:Ne,featureFlags:{},sessionReplayBlockClass:T,sessionReplayBlockSelector:m,sessionReplayIgnoreClass:o,sessionReplayIgnoreSelector:c,sessionReplayMaskTextClass:p,sessionReplayMaskAllInputs:C,sessionReplayMaskInputOptions:F,sessionReplayCollectFonts:Me,sessionReplaySampling:xe,sessionReplaySlimDOMOptions:Be,sessionReplaySampleRate:Oe},L=f;try{let $=`${t}/site/tracking-config/${r}`,ce=await fetch($,{method:"GET",credentials:"omit"});if(ce.ok){let y=await ce.json();L={...f,autoTrackPageview:y.trackInitialPageView??f.autoTrackPageview,autoTrackSpa:y.trackSpaNavigation??f.autoTrackSpa,trackQuerystring:y.trackUrlParams??f.trackQuerystring,trackOutbound:y.trackOutbound??f.trackOutbound,enableWebVitals:y.webVitals??f.enableWebVitals,trackErrors:y.trackErrors??f.trackErrors,enableSessionReplay:y.sessionReplay??f.enableSessionReplay,trackButtonClicks:y.trackButtonClicks??f.trackButtonClicks,trackCopy:y.trackCopy??f.trackCopy,trackFormInteractions:y.trackFormInteractions??f.trackFormInteractions},y.persistentClientIds?L.persistentClientId=$e(i):Ve(i)}else console.warn("Failed to fetch tracking config from API, using defaults")}catch($){console.warn("Error fetching tracking config:",$)}return L.featureFlags=await Ge(t,r,i,s),L}var pe="rybbit-replay-sampled";function Ke(n){if(n>=100)return!0;if(n<=0)return!1;try{let e=sessionStorage.getItem(pe);if(e!==null)return e==="1";let t=Math.random()*100{let r=document.createElement("script");r.src=`${this.config.analyticsHost}/replay.js`,r.async=!1,r.onload=()=>{e()},r.onerror=()=>t(new Error("Failed to load rrweb")),document.head.appendChild(r)})}startRecording(){if(!(this.isRecording||!window.rrweb||!this.config.enableSessionReplay))try{let e={mousemove:!1,mouseInteraction:{MouseUp:!1,MouseDown:!1,Click:!0,ContextMenu:!1,DblClick:!0,Focus:!0,Blur:!0,TouchStart:!1,TouchEnd:!1},scroll:500,input:"last",media:800},t={script:!1,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaDescKeywords:!0,headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaAuthorship:!0,headMetaVerification:!0},r={emit:i=>{this.addEvent({type:i.type,data:i.data,timestamp:i.timestamp||Date.now()})},recordCanvas:!1,checkoutEveryNms:6e4,checkoutEveryNth:500,blockClass:this.config.sessionReplayBlockClass??"rr-block",blockSelector:this.config.sessionReplayBlockSelector??null,ignoreClass:this.config.sessionReplayIgnoreClass??"rr-ignore",ignoreSelector:this.config.sessionReplayIgnoreSelector??null,maskTextClass:this.config.sessionReplayMaskTextClass??"rr-mask",maskAllInputs:this.config.sessionReplayMaskAllInputs??!0,maskInputOptions:this.config.sessionReplayMaskInputOptions??{password:!0,email:!0},collectFonts:this.config.sessionReplayCollectFonts??!0,sampling:this.config.sessionReplaySampling??e,slimDOMOptions:this.config.sessionReplaySlimDOMOptions??t};this.config.sessionReplayMaskTextSelectors&&this.config.sessionReplayMaskTextSelectors.length>0&&(r.maskTextSelector=this.config.sessionReplayMaskTextSelectors.join(", ")),this.stopRecordingFn=window.rrweb.record(r),this.isRecording=!0,this.setupBatchTimer()}catch{}}stopRecording(){this.isRecording&&(this.stopRecordingFn&&this.stopRecordingFn(),this.isRecording=!1,this.clearBatchTimer(),this.eventBuffer.length>0&&this.flushEvents())}isActive(){return this.isRecording}addEvent(e){this.eventBuffer.push(e),this.eventBuffer.length>=this.config.sessionReplayBatchSize&&this.flushEvents()}setupBatchTimer(){this.clearBatchTimer(),this.batchTimer=window.setInterval(()=>{this.eventBuffer.length>0&&this.flushEvents()},this.config.sessionReplayBatchInterval)}clearBatchTimer(){this.batchTimer&&(clearInterval(this.batchTimer),this.batchTimer=void 0)}async flushEvents(){if(this.eventBuffer.length===0)return;let e=[...this.eventBuffer];this.eventBuffer=[];let t={userId:this.userId,...this.config.persistentClientId&&{anonymousId:this.config.persistentClientId},events:e,metadata:{pageUrl:window.location.href,viewportWidth:screen.width,viewportHeight:screen.height,language:navigator.language}};try{await this.sendBatch(t)}catch{this.eventBuffer.unshift(...e)}}updateUserId(e){e!==this.userId&&(this.eventBuffer.length>0&&this.flushEvents(),this.userId=e)}onPageChange(){this.isRecording&&this.flushEvents()}cleanup(){this.stopRecording()}};var b={automationApi:1,webdriver:1,zeroOuterDimensions:2,missingChrome:4,swiftShader:8,emptyPlugins:16,defaultViewport800x600:32,defaultViewport1024x768:64,impossibleDimensions:128,outerDimensionsWeird:256,pluginApiAbsence:512},ge=null,Je=10;function he(){return me().score}function fe(){return me().mask}function me(){return ge??(ge=qe()),ge}function qe(){let n=0,e=0;function t(r,i){(e&r)===0&&(e|=r,n+=i)}try{let r=navigator.userAgent,i=/Chrome\//.test(r)&&!/\bwv\b|; wv\)/.test(r),s=/Windows NT|Macintosh|X11|Linux x86_64/.test(r)&&!/Mobile|Android|iPhone|iPad/.test(r),a=Number(window.screen?.width),l=Number(window.screen?.height),d=Number(window.outerWidth),u=Number(window.outerHeight),g=Number(window.innerWidth),h=Number(window.innerHeight),m=["__webdriver_evaluate","__selenium_evaluate","__webdriver_script_function","__webdriver_script_func","__webdriver_script_fn","__fxdriver_evaluate","__driver_unwrapped","__webdriver_unwrapped","__driver_evaluate","__selenium_unwrapped","__fxdriver_unwrapped","_phantom","callPhantom","__nightmare","domAutomation","domAutomationController"].some(c=>c in window||c in document);(navigator.webdriver===!0||m)&&t(b.automationApi,3),(u===0||d===0)&&t(b.zeroOuterDimensions,2),(!Number.isFinite(a)||!Number.isFinite(l)||a<=0||l<=0||a>1e5||l>1e5)&&t(b.impossibleDimensions,3),s&&a===800&&l===600&&t(b.defaultViewport800x600,3),s&&a===1024&&l===768&&t(b.defaultViewport1024x768,3),Number.isFinite(d)&&Number.isFinite(u)&&Number.isFinite(g)&&Number.isFinite(h)&&d>0&&u>0&&g>0&&h>0&&(d+8this.sendSessionReplayBatch(e)),await this.sessionReplayRecorder.initialize()}catch(e){console.error("Failed to initialize session replay:",e)}}async sendSessionReplayBatch(e){try{await fetch(`${this.config.analyticsHost}/session-replay/record/${this.config.siteId}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e),mode:"cors",keepalive:!1})}catch(t){throw console.error("Failed to send session replay batch:",t),t}}createBasePayload(){let e=new URL(window.location.href),t=e.pathname;if(e.hash&&e.hash.startsWith("#/")&&(t=e.hash.substring(1)),V(t,this.config.skipPatterns))return null;let r=V(t,this.config.maskPatterns);r&&(t=r);let i={site_id:this.config.siteId,hostname:e.hostname,pathname:t,querystring:this.config.trackQuerystring?e.search:"",screenWidth:screen.width,screenHeight:screen.height,language:navigator.language,page_title:document.title,referrer:document.referrer,_bs:he(),_bsm:fe()};this.customUserId&&(i.user_id=this.customUserId),this.config.persistentClientId&&(i.anonymous_id=this.config.persistentClientId),this.config.tag&&(i.tag=this.config.tag);let s=this.getFeatureFlagEventPayload();return Object.keys(s).length>0&&(i.feature_flags=s),i}async sendTrackingData(e){try{await fetch(`${this.config.analyticsHost}/track`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e),mode:"cors",keepalive:!0})}catch(t){console.error("Failed to send tracking data:",t)}}track(e,t="",r={}){if(e==="custom_event"&&(!t||typeof t!="string")){console.error("Event name is required and must be a string for custom events");return}let i=this.createBasePayload();if(!i)return;let a={...i,type:e,event_name:t,properties:["custom_event","outbound","error","button_click","copy","form_submit","input_change"].includes(e)?JSON.stringify(r):void 0};this.sendTrackingData(a)}trackPageview(){this.track("pageview")}trackEvent(e,t={}){this.track("custom_event",e,t)}getFeatureFlag(e,t){let r=this.config.featureFlags?.[e];if(!r)return t;let i=`${e}:${r.version}:${this.serializeFeatureFlagValue(r.value)}`;return this.exposedFeatureFlags.has(i)||(this.exposedFeatureFlags.add(i),this.trackEvent("feature_flag_exposure",{key:e,value:this.serializeFeatureFlagValue(r.value),version:r.version,reason:r.reason})),r.value}getFeatureFlags(){return Object.fromEntries(Object.entries(this.config.featureFlags||{}).map(([e,t])=>[e,t.value]))}getFeatureFlagPayload(e,t){let r=this.config.featureFlags?.[e];return!r||r.payload===void 0?t:r.payload}getFeatureFlagPayloads(){return Object.fromEntries(Object.entries(this.config.featureFlags||{}).filter(([,e])=>e.payload!==void 0).map(([e,t])=>[e,t.payload]))}trackOutbound(e,t="",r="_self"){this.track("outbound","",{url:e,text:t,target:r})}trackWebVitals(e){let t=this.createBasePayload();if(!t)return;let r={...t,type:"performance",event_name:"web-vitals",...e};this.sendTrackingData(r)}trackError(e,t={}){let r=e?.message||"";if(r.includes("ResizeObserver loop completed with undelivered notifications")||r.includes("ResizeObserver loop limit exceeded"))return;let i=window.location.origin,s=t.filename||"",a=e.stack||"";if(s)try{if(new URL(s).origin!==i)return}catch{}else if(a&&!a.includes(i))return;let d=[e.name||"Error",r,t.filename||"",t.lineno??"",t.colno??""].join("|"),u=Date.now(),g=6e4,h=this.errorDedupeCache.get(d);if(h&&u-hg){for(let[o,c]of this.errorDedupeCache.entries())u-c>T&&this.errorDedupeCache.delete(o);this.errorDedupeLastCleanup=u}let m={message:e.message?.substring(0,500)||"Unknown error",stack:a.substring(0,2e3)||""};if(s&&(m.fileName=s),t.lineno){let o=typeof t.lineno=="string"?parseInt(t.lineno,10):t.lineno;o&&o!==0&&(m.lineNumber=o)}if(t.colno){let o=typeof t.colno=="string"?parseInt(t.colno,10):t.colno;o&&o!==0&&(m.columnNumber=o)}for(let o in t)!["lineno","colno"].includes(o)&&t[o]!==void 0&&(m[o]=t[o]);this.track("error",e.name||"Error",m)}trackButtonClick(e){this.track("button_click","",e)}trackCopy(e){this.track("copy","",e)}trackFormSubmit(e){this.track("form_submit","",e)}trackInputChange(e){this.track("input_change","",e)}identify(e,t){if(typeof e!="string"||e.trim()===""){console.error("User ID must be a non-empty string");return}this.customUserId=e.trim();try{localStorage.setItem(`${this.config.namespace}-user-id`,this.customUserId)}catch{console.warn("Could not persist user ID to localStorage")}this.sendIdentifyEvent(this.customUserId,t,!0).then(()=>this.refreshFeatureFlags()),this.sessionReplayRecorder&&this.sessionReplayRecorder.updateUserId(this.customUserId)}setTraits(e){if(!e||typeof e!="object"){console.error("Traits must be an object");return}let t=this.customUserId;if(!t){console.warn("Cannot set traits without identifying user first. Call identify() first.");return}this.sendIdentifyEvent(t,e,!1).then(()=>this.refreshFeatureFlags())}async sendIdentifyEvent(e,t,r=!0){try{await fetch(`${this.config.analyticsHost}/identify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({site_id:this.config.siteId,user_id:e,traits:t,is_new_identify:r}),mode:"cors",keepalive:!0})}catch(i){console.error("Failed to send identify event:",i)}}clearUserId(){this.customUserId=null;try{localStorage.removeItem(`${this.config.namespace}-user-id`)}catch{}this.sessionReplayRecorder&&this.sessionReplayRecorder.updateUserId(""),this.refreshFeatureFlags()}getUserId(){return this.customUserId}startSessionReplay(){this.sessionReplayRecorder?this.sessionReplayRecorder.startRecording():console.warn("Session replay not initialized")}stopSessionReplay(){this.sessionReplayRecorder&&this.sessionReplayRecorder.stopRecording()}isSessionReplayActive(){return this.sessionReplayRecorder?.isActive()??!1}onPageChange(){this.refreshFeatureFlags(),this.sessionReplayRecorder&&this.sessionReplayRecorder.onPageChange()}cleanup(){this.sessionReplayRecorder&&this.sessionReplayRecorder.cleanup()}};var Ee=-1,P=n=>{addEventListener("pageshow",(e=>{e.persisted&&(Ee=e.timeStamp,n(e))}),!0)},v=(n,e,t,r)=>{let i,s;return a=>{e.value>=0&&(a||r)&&(s=e.value-(i??0),(s||i===void 0)&&(i=e.value,e.delta=s,e.rating=((l,d)=>l>d[1]?"poor":l>d[0]?"needs-improvement":"good")(e.value,t),n(e)))}},Z=n=>{requestAnimationFrame((()=>requestAnimationFrame((()=>n()))))},ee=()=>{let n=performance.getEntriesByType("navigation")[0];if(n&&n.responseStart>0&&n.responseStartee()?.activationStart??0,k=(n,e=-1)=>{let t=ee(),r="navigate";return Ee>=0?r="back-forward-cache":t&&(document.prerendering||_()>0?r="prerender":document.wasDiscarded?r="restore":t.type&&(r=t.type.replace(/_/g,"-"))),{name:n,value:e,rating:"good",delta:0,entries:[],id:`v5-${Date.now()}-${Math.floor(8999999999999*Math.random())+1e12}`,navigationType:r}},j=new WeakMap;function te(n,e){return j.get(n)||j.set(n,new e),j.get(n)}var K=class{constructor(){R(this,"t");R(this,"i",0);R(this,"o",[])}h(e){if(e.hadRecentInput)return;let t=this.o[0],r=this.o.at(-1);this.i&&t&&r&&e.startTime-r.startTime<1e3&&e.startTime-t.startTime<5e3?(this.i+=e.value,this.o.push(e)):(this.i=e.value,this.o=[e]),this.t?.(e)}},A=(n,e,t={})=>{try{if(PerformanceObserver.supportedEntryTypes.includes(n)){let r=new PerformanceObserver((i=>{Promise.resolve().then((()=>{e(i.getEntries())}))}));return r.observe({type:n,buffered:!0,...t}),r}}catch{}},re=n=>{let e=!1;return()=>{e||(n(),e=!0)}},I=-1,Te=new Set,ye=()=>document.visibilityState!=="hidden"||document.prerendering?1/0:0,J=n=>{if(document.visibilityState==="hidden"){if(n.type==="visibilitychange")for(let e of Te)e();isFinite(I)||(I=n.type==="visibilitychange"?n.timeStamp:0,removeEventListener("prerenderingchange",J,!0))}},O=()=>{if(I<0){let n=_();I=(document.prerendering?void 0:globalThis.performance.getEntriesByType("visibility-state").filter((t=>t.name==="hidden"&&t.startTime>n))[0]?.startTime)??ye(),addEventListener("visibilitychange",J,!0),addEventListener("prerenderingchange",J,!0),P((()=>{setTimeout((()=>{I=ye()}))}))}return{get firstHiddenTime(){return I},onHidden(n){Te.add(n)}}},N=n=>{document.prerendering?addEventListener("prerenderingchange",(()=>n()),!0):n()},be=[1800,3e3],ne=(n,e={})=>{N((()=>{let t=O(),r,i=k("FCP"),s=A("paint",(a=>{for(let l of a)l.name==="first-contentful-paint"&&(s.disconnect(),l.startTime{i=k("FCP"),r=v(n,i,be,e.reportAllChanges),Z((()=>{i.value=performance.now()-a.timeStamp,r(!0)}))})))}))},ve=[.1,.25],Ie=(n,e={})=>{let t=O();ne(re((()=>{let r,i=k("CLS",0),s=te(e,K),a=d=>{for(let u of d)s.h(u);s.i>i.value&&(i.value=s.i,i.entries=s.o,r())},l=A("layout-shift",a);l&&(r=v(n,i,ve,e.reportAllChanges),t.onHidden((()=>{a(l.takeRecords()),r(!0)})),P((()=>{s.i=0,i=k("CLS",0),r=v(n,i,ve,e.reportAllChanges),Z((()=>r()))})),setTimeout(r))})))},Pe=0,G=1/0,B=0,Qe=n=>{for(let e of n)e.interactionId&&(G=Math.min(G,e.interactionId),B=Math.max(B,e.interactionId),Pe=B?(B-G)/7+1:0)},q,ke=()=>q?Pe:performance.interactionCount??0,Ye=()=>{"interactionCount"in performance||q||(q=A("event",Qe,{type:"event",buffered:!0,durationThreshold:0}))},we=0,X=class{constructor(){R(this,"u",[]);R(this,"l",new Map);R(this,"m");R(this,"p")}v(){we=ke(),this.u.length=0,this.l.clear()}L(){let e=Math.min(this.u.length-1,Math.floor((ke()-we)/50));return this.u[e]}h(e){if(this.m?.(e),!e.interactionId&&e.entryType!=="first-input")return;let t=this.u.at(-1),r=this.l.get(e.interactionId);if(r||this.u.length<10||e.duration>t.P){if(r?e.duration>r.P?(r.entries=[e],r.P=e.duration):e.duration===r.P&&e.startTime===r.entries[0].startTime&&r.entries.push(e):(r={id:e.interactionId,entries:[e],P:e.duration},this.l.set(r.id,r),this.u.push(r)),this.u.sort(((i,s)=>s.P-i.P)),this.u.length>10){let i=this.u.splice(10);for(let s of i)this.l.delete(s.id)}this.p?.(r)}}},_e=n=>{let e=globalThis.requestIdleCallback||setTimeout;document.visibilityState==="hidden"?n():(n=re(n),addEventListener("visibilitychange",n,{once:!0,capture:!0}),e((()=>{n(),removeEventListener("visibilitychange",n,{capture:!0})})))},Re=[200,500],Ae=(n,e={})=>{if(!globalThis.PerformanceEventTiming||!("interactionId"in PerformanceEventTiming.prototype))return;let t=O();N((()=>{Ye();let r,i=k("INP"),s=te(e,X),a=d=>{_e((()=>{for(let g of d)s.h(g);let u=s.L();u&&u.P!==i.value&&(i.value=u.P,i.entries=u.entries,r())}))},l=A("event",a,{durationThreshold:e.durationThreshold??40});r=v(n,i,Re,e.reportAllChanges),l&&(l.observe({type:"first-input",buffered:!0}),t.onHidden((()=>{a(l.takeRecords()),r(!0)})),P((()=>{s.v(),i=k("INP"),r=v(n,i,Re,e.reportAllChanges)})))}))},Q=class{constructor(){R(this,"m")}h(e){this.m?.(e)}},Ce=[2500,4e3],Fe=(n,e={})=>{N((()=>{let t=O(),r,i=k("LCP"),s=te(e,Q),a=d=>{e.reportAllChanges||(d=d.slice(-1));for(let u of d)s.h(u),u.startTime{a(l.takeRecords()),l.disconnect(),r(!0)})),u=g=>{g.isTrusted&&(_e(d),removeEventListener(g.type,u,{capture:!0}))};for(let g of["keydown","click","visibilitychange"])addEventListener(g,u,{capture:!0});P((g=>{i=k("LCP"),r=v(n,i,Ce,e.reportAllChanges),Z((()=>{i.value=performance.now()-g.timeStamp,r(!0)}))}))}}))},Se=[800,1800],Y=n=>{document.prerendering?N((()=>Y(n))):document.readyState!=="complete"?addEventListener("load",(()=>Y(n)),!0):setTimeout(n)},Le=(n,e={})=>{let t=k("TTFB"),r=v(n,t,Se,e.reportAllChanges);Y((()=>{let i=ee();i&&(t.value=Math.max(i.responseStart-_(),0),t.entries=[i],r(!0),P((()=>{t=k("TTFB",0),r=v(n,t,Se,e.reportAllChanges),r(!0)})))}))};var D=class{constructor(e){this.data={lcp:null,cls:null,inp:null,fcp:null,ttfb:null};this.sent=!1;this.timeout=null;this.onReadyCallback=null;this.onReadyCallback=e}initialize(){try{Fe(this.collectMetric.bind(this)),Ie(this.collectMetric.bind(this)),Ae(this.collectMetric.bind(this)),ne(this.collectMetric.bind(this)),Le(this.collectMetric.bind(this)),this.timeout=setTimeout(()=>{this.sent||this.sendData()},2e4),window.addEventListener("beforeunload",()=>{this.sent||this.sendData()})}catch(e){console.warn("Error initializing web vitals tracking:",e)}}collectMetric(e){if(this.sent)return;let t=e.name.toLowerCase();this.data[t]=e.value,Object.values(this.data).every(i=>i!==null)&&this.sendData()}sendData(){this.sent||(this.sent=!0,this.timeout&&(clearTimeout(this.timeout),this.timeout=null),this.onReadyCallback&&this.onReadyCallback(this.data))}getData(){return{...this.data}}};var Ze=1e3,U=class{constructor(e,t){this.lastClickAt=new WeakMap;this.tracker=e,this.config=t}initialize(){document.addEventListener("click",this.handleClick.bind(this),!0)}handleClick(e){let t=e.target;this.config.trackButtonClicks&&this.isButton(t)&&this.trackButtonClick(t)}isButton(e){if(e.tagName==="BUTTON"||e.getAttribute("role")==="button")return!0;if(e.tagName==="INPUT"){let i=e.type?.toLowerCase();if(i==="submit"||i==="button")return!0}let t=e.parentElement,r=0;for(;t&&r<3;){if(t.tagName==="BUTTON"||t.getAttribute("role")==="button")return!0;t=t.parentElement,r++}return!1}trackButtonClick(e){let t=this.findButton(e);if(!t||t.hasAttribute("data-rybbit-event"))return;let r=Date.now(),i=this.lastClickAt.get(t);if(i!==void 0&&r-i500&&{textLength:r},sourceElement:s.tagName.toLowerCase()};this.tracker.trackCopy(a)}cleanup(){document.removeEventListener("copy",this.handleCopy.bind(this))}};var W=class{constructor(e,t){this.tracker=e,this.config=t,this.boundHandleSubmit=this.handleSubmit.bind(this),this.boundHandleChange=this.handleChange.bind(this)}initialize(){document.addEventListener("submit",this.boundHandleSubmit,!0),document.addEventListener("change",this.boundHandleChange,!0)}cleanup(){document.removeEventListener("submit",this.boundHandleSubmit,!0),document.removeEventListener("change",this.boundHandleChange,!0)}handleSubmit(e){let t=e.target;if(t.tagName!=="FORM")return;let r={formId:t.id||"",formName:t.name||"",formAction:t.action||"",method:(t.method||"get").toUpperCase(),fieldCount:t.elements.length,ariaLabel:t.getAttribute("aria-label")||void 0,...this.extractDataAttributes(t)};this.tracker.trackFormSubmit(r)}handleChange(e){let t=e.target,r=t.tagName.toUpperCase();if(!["INPUT","SELECT","TEXTAREA"].includes(r)||t.disabled||t.readOnly)return;if(r==="INPUT"){let a=t.type?.toLowerCase();if(a==="hidden"||a==="password")return}let i=t.name||t.id||t.getAttribute("aria-label")||t.placeholder||"",s={element:r.toLowerCase(),inputType:r==="INPUT"?t.type?.toLowerCase():void 0,inputName:i,formId:t.form?.id||void 0,formName:t.form?.name||void 0,...this.extractDataAttributes(t)};this.tracker.trackInputChange(s)}extractDataAttributes(e){let t={};for(let r of e.attributes)if(r.name.startsWith("data-rybbit-prop-")){let i=r.name.replace("data-rybbit-prop-","");t[i]=r.value}return t}};(async function(){let n=document.currentScript;if(!n){console.error("Could not find current script tag");return}let e=n.getAttribute("data-namespace")||"rybbit",t=`disable-${e}`;if(window.__RYBBIT_OPTOUT__||localStorage.getItem(t)!==null){window[e]={pageview:()=>{},event:()=>{},error:()=>{},trackOutbound:()=>{},identify:()=>{},setTraits:()=>{},clearUserId:()=>{},getUserId:()=>null,flag:(o,c)=>c,flagPayload:(o,c)=>c,flags:()=>({}),flagPayloads:()=>({}),onReady:()=>{},startSessionReplay:()=>{},stopSessionReplay:()=>{},isSessionReplayActive:()=>!1};return}let r=[],i=o=>(...c)=>{r.push([o,c])};window[e]={pageview:i("pageview"),event:i("event"),error:i("error"),trackOutbound:i("trackOutbound"),identify:i("identify"),setTraits:i("setTraits"),clearUserId:i("clearUserId"),getUserId:()=>null,flag:(o,c)=>c,flagPayload:(o,c)=>c,flags:()=>({}),flagPayloads:()=>({}),onReady:i("onReady"),startSessionReplay:i("startSessionReplay"),stopSessionReplay:i("stopSessionReplay"),isSessionReplayActive:()=>!1};let s=await de(n);if(!s)return;let a=new x(s);s.enableWebVitals&&new D(c=>{a.trackWebVitals(c)}).initialize();let l=null,d=null,u=null;s.trackButtonClicks&&(l=new U(a,s),l.initialize()),s.trackCopy&&(d=new H(a),d.initialize()),s.trackFormInteractions&&(u=new W(a,s),u.initialize()),s.trackErrors&&(window.addEventListener("error",o=>{a.trackError(o.error||new Error(o.message),{filename:o.filename,lineno:o.lineno,colno:o.colno})}),window.addEventListener("unhandledrejection",o=>{let c=o.reason instanceof Error?o.reason:new Error(String(o.reason));a.trackError(c,{type:"unhandledrejection"})}));let g=()=>a.trackPageview(),h=s.debounceDuration>0?le(g,s.debounceDuration):g;function T(){if(document.addEventListener("click",function(o){let c=o.target;for(;c&&c!==document.documentElement;){if(c.hasAttribute("data-rybbit-event")){let p=c.getAttribute("data-rybbit-event");if(p){let w={};for(let C of c.attributes)if(C.name.startsWith("data-rybbit-prop-")){let S=C.name.replace("data-rybbit-prop-","");w[S]=C.value}a.trackEvent(p,w)}break}c=c.parentElement}if(s.trackOutbound){let p=o.target.closest("a");p?.href&&ue(p.href)&&a.trackOutbound(p.href,p.innerText||p.textContent||"",p.target||"_self")}}),s.autoTrackSpa){let o=history.pushState,c=history.replaceState;history.pushState=function(...p){o.apply(this,p),h(),a.onPageChange()},history.replaceState=function(...p){c.apply(this,p),h(),a.onPageChange()},window.addEventListener("popstate",()=>{h(),a.onPageChange()}),window.addEventListener("hashchange",()=>{h(),a.onPageChange()})}}window[s.namespace]={pageview:()=>a.trackPageview(),event:(o,c={})=>a.trackEvent(o,c),error:(o,c={})=>a.trackError(o,c),trackOutbound:(o,c="",p="_self")=>a.trackOutbound(o,c,p),identify:(o,c)=>a.identify(o,c),setTraits:o=>a.setTraits(o),clearUserId:()=>a.clearUserId(),getUserId:()=>a.getUserId(),flag:(o,c)=>a.getFeatureFlag(o,c),flagPayload:(o,c)=>a.getFeatureFlagPayload(o,c),flags:()=>a.getFeatureFlags(),flagPayloads:()=>a.getFeatureFlagPayloads(),onReady:o=>o(window[s.namespace]),startSessionReplay:()=>a.startSessionReplay(),stopSessionReplay:()=>a.stopSessionReplay(),isSessionReplayActive:()=>a.isSessionReplayActive()};let m=window[s.namespace];for(let[o,c]of r)m[o](...c);T(),window.addEventListener("beforeunload",()=>{l?.cleanup(),d?.cleanup(),a.cleanup()}),s.autoTrackPageview&&a.trackPageview()})();})(); diff --git a/server/src/analytics-script/config.ts b/server/src/analytics-script/config.ts index 38fb7d402..f40f63dfb 100644 --- a/server/src/analytics-script/config.ts +++ b/server/src/analytics-script/config.ts @@ -28,6 +28,33 @@ function getOrCreateVisitorId(namespace: string): string { } } +// Only created/persisted when the site has opted into persistentClientIds +// (consented, since this stores an identifier on the visitor's device). +function getOrCreatePersistentClientId(namespace: string): string | undefined { + const key = `${namespace}-persistent-id`; + + try { + const stored = localStorage.getItem(key); + if (stored) return stored; + + const id = createVisitorId(); + localStorage.setItem(key, id); + return id; + } catch (e) { + return undefined; + } +} + +// If the site disabled persistentClientIds after previously enabling it, drop +// any identifier already stored — it must not linger once consent is withdrawn. +function clearPersistentClientId(namespace: string): void { + try { + localStorage.removeItem(`${namespace}-persistent-id`); + } catch (e) { + // localStorage unavailable; nothing to clear + } +} + function getIdentifiedUserId(namespace: string): string | undefined { try { return localStorage.getItem(`${namespace}-user-id`) || undefined; @@ -228,6 +255,12 @@ export async function parseScriptConfig(scriptTag: HTMLScriptElement): Promise { events: [{ data: { user: "employee-bob" } }], }); }); + + it("attaches anonymousId to batches when a persistent client id is configured", async () => { + const persistentConfig: ScriptConfig = { ...config, persistentClientId: "visitor-abc-123" }; + const persistentSendBatch = vi.fn().mockResolvedValue(undefined); + const persistentRecorder = new SessionReplayRecorder(persistentConfig, "", persistentSendBatch); + await persistentRecorder.initialize(); + + emit({ type: 2, data: {}, timestamp: 1_700_000_002_000 }); + persistentRecorder.stopRecording(); + + await vi.waitFor(() => expect(persistentSendBatch).toHaveBeenCalledTimes(1)); + expect(persistentSendBatch.mock.calls[0][0]).toMatchObject({ anonymousId: "visitor-abc-123" }); + + persistentRecorder.cleanup(); + }); + + it("omits anonymousId when no persistent client id is configured", async () => { + emit({ type: 2, data: { user: "employee-alice" }, timestamp: 1_700_000_003_000 }); + recorder.stopRecording(); + + await vi.waitFor(() => expect(sendBatch).toHaveBeenCalledTimes(1)); + expect(sendBatch.mock.calls[0][0].anonymousId).toBeUndefined(); + }); }); diff --git a/server/src/analytics-script/sessionReplay.ts b/server/src/analytics-script/sessionReplay.ts index 1b8d0c738..175bc49ee 100644 --- a/server/src/analytics-script/sessionReplay.ts +++ b/server/src/analytics-script/sessionReplay.ts @@ -239,6 +239,7 @@ export class SessionReplayRecorder { const batch: SessionReplayBatch = { userId: this.userId, + ...(this.config.persistentClientId && { anonymousId: this.config.persistentClientId }), events, metadata: { pageUrl: window.location.href, diff --git a/server/src/analytics-script/tracking.ts b/server/src/analytics-script/tracking.ts index 998174288..72d0f79d1 100644 --- a/server/src/analytics-script/tracking.ts +++ b/server/src/analytics-script/tracking.ts @@ -169,6 +169,10 @@ export class Tracker { payload.user_id = this.customUserId; } + if (this.config.persistentClientId) { + payload.anonymous_id = this.config.persistentClientId; + } + if (this.config.tag) { payload.tag = this.config.tag; } diff --git a/server/src/analytics-script/types.ts b/server/src/analytics-script/types.ts index c2fb34d7b..c1cfd3bec 100644 --- a/server/src/analytics-script/types.ts +++ b/server/src/analytics-script/types.ts @@ -3,6 +3,10 @@ export interface ScriptConfig { analyticsHost: string; siteId: string; visitorId: string; + // Persistent localStorage identifier, only present when the site has opted + // into persistentClientIds. Sent as anonymous_id on tracking payloads and + // replay batches for stronger identification accuracy than IP+UA. + persistentClientId?: string; debounceDuration: number; autoTrackPageview: boolean; autoTrackSpa: boolean; @@ -59,6 +63,7 @@ export interface BasePayload { page_title: string; referrer: string; user_id?: string; + anonymous_id?: string; tag?: string; feature_flags?: Record; _bs?: number; // Client-side weighted bot detection score @@ -159,6 +164,7 @@ export interface SessionReplayEvent { export interface SessionReplayBatch { userId: string; + anonymousId?: string; events: SessionReplayEvent[]; metadata?: { pageUrl: string; diff --git a/server/src/api/sessionReplay/recordSessionReplay.ts b/server/src/api/sessionReplay/recordSessionReplay.ts index 11c559e31..2a39f4358 100644 --- a/server/src/api/sessionReplay/recordSessionReplay.ts +++ b/server/src/api/sessionReplay/recordSessionReplay.ts @@ -9,6 +9,7 @@ import { decideSiteExclusion } from "../../services/sites/siteExclusionDecision. const recordSessionReplaySchema = z.object({ userId: z.string(), + anonymousId: z.string().min(1).max(255).optional(), events: z.array( z.object({ type: z.union([z.string(), z.number()]), @@ -120,6 +121,7 @@ export async function recordSessionReplay( ipAddress: requestIP, origin, referrer, + persistentClientIds: siteConfiguration.persistentClientIds, }); return reply.send({ success: true }); diff --git a/server/src/api/sites/getTrackingConfig.ts b/server/src/api/sites/getTrackingConfig.ts index b0f1c19ef..c2dad332d 100644 --- a/server/src/api/sites/getTrackingConfig.ts +++ b/server/src/api/sites/getTrackingConfig.ts @@ -32,6 +32,7 @@ export async function getTrackingConfig(request: FastifyRequest<{ Params: { site trackButtonClicks: config.trackButtonClicks || false, trackCopy: config.trackCopy || false, trackFormInteractions: config.trackFormInteractions || false, + persistentClientIds: config.persistentClientIds || false, }); } catch (error) { request.log.error({ err: error }, "Error getting tracking config"); diff --git a/server/src/api/sites/updateSiteConfig.ts b/server/src/api/sites/updateSiteConfig.ts index 26a46a092..15fd1fdfd 100644 --- a/server/src/api/sites/updateSiteConfig.ts +++ b/server/src/api/sites/updateSiteConfig.ts @@ -10,6 +10,7 @@ const updateSiteConfigSchema = z.object({ saltUserIds: z.boolean().optional(), blockBots: z.boolean().optional(), firstPartyProxy: z.boolean().optional(), + persistentClientIds: z.boolean().optional(), domain: z.string().min(1).max(253).optional(), excludedIPs: z.array(z.string().trim().min(1)).max(100).optional(), excludedCountries: z diff --git a/server/src/db/postgres/schema.ts b/server/src/db/postgres/schema.ts index 776a65e63..9a201c5c6 100644 --- a/server/src/db/postgres/schema.ts +++ b/server/src/db/postgres/schema.ts @@ -79,6 +79,11 @@ export const sites = pgTable( // nginx, ...) fronts their tracking traffic, so forwarded headers carry the // real visitor IP and must win over the connecting edge IP. firstPartyProxy: boolean("first_party_proxy").default(false), + // Site owner opts into a persistent client-side identifier (localStorage, + // sent as anonymous_id) for stronger identification accuracy than the + // cookieless IP+UA fingerprint. Requires the site's own consent banner; + // mutually exclusive with saltUserIds (opposite privacy postures). + persistentClientIds: boolean("persistent_client_ids").default(false), excludedIPs: jsonb("excluded_ips").default([]), // Array of IP addresses/ranges to exclude excludedCountries: jsonb("excluded_countries").default([]), // Array of ISO country codes to exclude (e.g., ["US", "GB"]) excludedPaths: jsonb("excluded_paths").default([]).$type(), // Array of pathname glob patterns to exclude (e.g., ["/admin/*", "/preview"]) diff --git a/server/src/lib/auth-utils.test.ts b/server/src/lib/auth-utils.test.ts index 3c96c07a9..4236e5a9a 100644 --- a/server/src/lib/auth-utils.test.ts +++ b/server/src/lib/auth-utils.test.ts @@ -90,6 +90,7 @@ CREATE TABLE "sites" ( "saltUserIds" boolean DEFAULT false, "blockBots" boolean DEFAULT true NOT NULL, "first_party_proxy" boolean DEFAULT false, + "persistent_client_ids" boolean DEFAULT false, "excluded_ips" jsonb DEFAULT '[]', "excluded_countries" jsonb DEFAULT '[]', "excluded_paths" jsonb DEFAULT '[]', diff --git a/server/src/lib/siteConfig.ts b/server/src/lib/siteConfig.ts index 8de765213..edd265d9c 100644 --- a/server/src/lib/siteConfig.ts +++ b/server/src/lib/siteConfig.ts @@ -14,6 +14,7 @@ export interface SiteConfigData { domain: string; blockBots: boolean; firstPartyProxy: boolean; + persistentClientIds: boolean; excludedIPs: string[]; excludedCountries: string[]; excludedPaths: string[]; @@ -102,6 +103,7 @@ class SiteConfig { domain: site.domain || "", blockBots: site.blockBots === undefined ? true : site.blockBots, firstPartyProxy: site.firstPartyProxy || false, + persistentClientIds: site.persistentClientIds || false, excludedIPs: Array.isArray(site.excludedIPs) ? site.excludedIPs : [], excludedCountries: Array.isArray(site.excludedCountries) ? site.excludedCountries : [], excludedPaths: Array.isArray(site.excludedPaths) ? site.excludedPaths : [], diff --git a/server/src/mcp/tools/sites.ts b/server/src/mcp/tools/sites.ts index 035624c0c..847d735d5 100644 --- a/server/src/mcp/tools/sites.ts +++ b/server/src/mcp/tools/sites.ts @@ -140,6 +140,12 @@ export function registerSiteTools(server: McpServer, api: RybbitApiClient, guard .describe( "Site is fronted by a first-party proxy (Cloudflare Worker, CloudFront, nginx); visitor IPs are read from forwarded headers" ), + persistentClientIds: z + .boolean() + .optional() + .describe( + "Use a persistent localStorage identifier (sent as anonymous_id) instead of the cookieless IP+UA fingerprint, for stronger identification accuracy. Requires the site's own consent banner, since this stores an identifier on the visitor's device; mutually exclusive with saltUserIds" + ), excludedIPs: z.array(z.string()).optional().describe("Replaces the exclusion list wholesale"), excludedCountries: z.array(z.string()).optional(), excludedPaths: z.array(z.string()).optional(), diff --git a/server/src/services/replay/sessionReplayIngestService.test.ts b/server/src/services/replay/sessionReplayIngestService.test.ts index 2f405181d..d60b4609d 100644 --- a/server/src/services/replay/sessionReplayIngestService.test.ts +++ b/server/src/services/replay/sessionReplayIngestService.test.ts @@ -3,6 +3,7 @@ import type { RecordSessionReplayRequest } from "../../types/sessionReplay.js"; const mocks = vi.hoisted(() => ({ generateUserId: vi.fn(), + generateUserIdFromClientId: vi.fn(), insert: vi.fn(), updateSession: vi.fn(), })); @@ -22,6 +23,7 @@ vi.mock("../sessions/sessionsService.js", () => ({ vi.mock("../userId/userIdService.js", () => ({ userIdService: { generateUserId: mocks.generateUserId, + generateUserIdFromClientId: mocks.generateUserIdFromClientId, }, })); @@ -31,10 +33,6 @@ vi.mock("../storage/r2StorageService.js", () => ({ }, })); -vi.mock("../../lib/siteConfig.js", () => ({ - siteConfig: {}, -})); - import { SessionReplayIngestService } from "./sessionReplayIngestService.js"; const requestMeta = { @@ -44,9 +42,10 @@ const requestMeta = { referrer: "", }; -function replayRequest(identifiedUserId: string): RecordSessionReplayRequest { +function replayRequest(identifiedUserId: string, anonymousId?: string): RecordSessionReplayRequest { return { userId: identifiedUserId, + anonymousId, events: [{ type: 2, data: { user: identifiedUserId }, timestamp: 1_700_000_000_000 }], }; } @@ -55,6 +54,7 @@ describe("SessionReplayIngestService identity", () => { beforeEach(() => { vi.clearAllMocks(); mocks.generateUserId.mockResolvedValue("shared-fingerprint"); + mocks.generateUserIdFromClientId.mockResolvedValue("persistent-fingerprint"); mocks.updateSession.mockImplementation( async ({ userId, identifiedUserId }: { userId: string; identifiedUserId?: string }) => ({ sessionId: `session-${userId}-${identifiedUserId || "anonymous"}`, @@ -100,4 +100,39 @@ describe("SessionReplayIngestService identity", () => { siteId: 42, }); }); + + it("uses the persistent client id when the site has opted in", async () => { + const service = new SessionReplayIngestService(); + + await service.recordEvents(42, replayRequest("", "visitor-abc"), { + ...requestMeta, + persistentClientIds: true, + }); + + expect(mocks.generateUserIdFromClientId).toHaveBeenCalledWith("visitor-abc", 42); + expect(mocks.generateUserId).not.toHaveBeenCalled(); + expect(mocks.updateSession).toHaveBeenCalledWith({ + userId: "persistent-fingerprint", + identifiedUserId: "", + siteId: 42, + }); + }); + + it("ignores a client-supplied anonymousId when the site has not opted in", async () => { + const service = new SessionReplayIngestService(); + + await service.recordEvents(42, replayRequest("", "visitor-abc"), requestMeta); + + expect(mocks.generateUserIdFromClientId).not.toHaveBeenCalled(); + expect(mocks.generateUserId).toHaveBeenCalledWith(requestMeta.ipAddress, requestMeta.userAgent, 42); + }); + + it("falls back to the IP+UA fingerprint when opted in but no anonymousId is sent", async () => { + const service = new SessionReplayIngestService(); + + await service.recordEvents(42, replayRequest(""), { ...requestMeta, persistentClientIds: true }); + + expect(mocks.generateUserIdFromClientId).not.toHaveBeenCalled(); + expect(mocks.generateUserId).toHaveBeenCalledWith(requestMeta.ipAddress, requestMeta.userAgent, 42); + }); }); diff --git a/server/src/services/replay/sessionReplayIngestService.ts b/server/src/services/replay/sessionReplayIngestService.ts index 9ca52c4b6..ed79fb283 100644 --- a/server/src/services/replay/sessionReplayIngestService.ts +++ b/server/src/services/replay/sessionReplayIngestService.ts @@ -6,13 +6,14 @@ import { parseTrackingData } from "./trackingUtils.js"; import { sessionsService } from "../sessions/sessionsService.js"; import { userIdService } from "../userId/userIdService.js"; import { r2Storage } from "../storage/r2StorageService.js"; -import { siteConfig } from "../../lib/siteConfig.js"; export interface RequestMetadata { userAgent: string; ipAddress: string; origin: string; referrer: string; + /** Whether the site has opted into persistent client IDs (see userIdService). */ + persistentClientIds?: boolean; } /** @@ -25,14 +26,17 @@ export class SessionReplayIngestService { request: RecordSessionReplayRequest, requestMeta?: RequestMetadata ): Promise { - const { userId: clientUserId, events, metadata } = request; - - // Always generate device fingerprint (anonymous user ID) server-side - const deviceFingerprint = await userIdService.generateUserId( - requestMeta?.ipAddress || "", - requestMeta?.userAgent || "", - siteId - ); + const { userId: clientUserId, anonymousId, events, metadata } = request; + + // Generate the device fingerprint (anonymous user ID) server-side. When the + // site has opted into persistent client IDs, prefer the client's persistent + // identifier so replay identity matches the fingerprint pageview tracking + // computes for the same visitor (createBasePayload) — otherwise the two + // would diverge and replay would fork onto its own session lineage. + const deviceFingerprint = + requestMeta?.persistentClientIds && anonymousId + ? await userIdService.generateUserIdFromClientId(anonymousId, siteId) + : await userIdService.generateUserId(requestMeta?.ipAddress || "", requestMeta?.userAgent || "", siteId); // Check if client provided an identified user ID (different from device fingerprint) const trimmedClientUserId = clientUserId?.trim() || ""; diff --git a/server/src/services/sites/siteConfigurationLifecycle.test.ts b/server/src/services/sites/siteConfigurationLifecycle.test.ts index cde277bee..d0187fa2b 100644 --- a/server/src/services/sites/siteConfigurationLifecycle.test.ts +++ b/server/src/services/sites/siteConfigurationLifecycle.test.ts @@ -118,4 +118,47 @@ describe("siteConfigurationLifecycle", () => { expect(state.deletes).toBe(0); expect(mocks.invalidate).not.toHaveBeenCalled(); }); + + describe("saltUserIds / persistentClientIds mutual exclusion", () => { + it("rejects enabling persistentClientIds while saltUserIds is already on", async () => { + state.site = { ...makeSite(), saltUserIds: true }; + + await expect(siteConfigurationLifecycle.update(1, { persistentClientIds: true })).rejects.toThrow( + "cannot both be enabled" + ); + expect(state.updates).toHaveLength(0); + }); + + it("rejects enabling saltUserIds while persistentClientIds is already on", async () => { + state.site = { ...makeSite(), persistentClientIds: true }; + + await expect(siteConfigurationLifecycle.update(1, { saltUserIds: true })).rejects.toThrow( + "cannot both be enabled" + ); + expect(state.updates).toHaveLength(0); + }); + + it("rejects enabling both in the same update", async () => { + await expect( + siteConfigurationLifecycle.update(1, { saltUserIds: true, persistentClientIds: true }) + ).rejects.toThrow("cannot both be enabled"); + expect(state.updates).toHaveLength(0); + }); + + it("allows enabling persistentClientIds when saltUserIds is disabled in the same update", async () => { + state.site = { ...makeSite(), saltUserIds: true }; + + await siteConfigurationLifecycle.update(1, { saltUserIds: false, persistentClientIds: true }); + + expect(state.updates).toHaveLength(1); + expect(state.updates[0]).toMatchObject({ saltUserIds: false, persistentClientIds: true }); + }); + + it("allows enabling persistentClientIds when saltUserIds was never on", async () => { + await siteConfigurationLifecycle.update(1, { persistentClientIds: true }); + + expect(state.updates).toHaveLength(1); + expect(state.updates[0]).toMatchObject({ persistentClientIds: true }); + }); + }); }); diff --git a/server/src/services/sites/siteConfigurationLifecycle.ts b/server/src/services/sites/siteConfigurationLifecycle.ts index fd0a8ca9a..5eb88ce6f 100644 --- a/server/src/services/sites/siteConfigurationLifecycle.ts +++ b/server/src/services/sites/siteConfigurationLifecycle.ts @@ -45,6 +45,7 @@ export type UpdateSiteConfigurationInput = { saltUserIds?: boolean; blockBots?: boolean; firstPartyProxy?: boolean; + persistentClientIds?: boolean; domain?: string; excludedIPs?: string[]; excludedCountries?: string[]; @@ -77,7 +78,8 @@ export type SiteLifecycleErrorCode = | "site_not_found" | "invalid_ip_patterns" | "empty_update" - | "domain_conflict"; + | "domain_conflict" + | "salting_persistent_id_conflict"; export class SiteLifecycleError extends Error { constructor( @@ -107,6 +109,7 @@ const DIRECT_UPDATE_FIELDS = [ "saltUserIds", "blockBots", "firstPartyProxy", + "persistentClientIds", "excludedIPs", "excludedCountries", "excludedPaths", @@ -162,6 +165,26 @@ function validateMobileFeatures(type: SiteType, input: Pick, + input: Pick +): void { + const nextSaltUserIds = input.saltUserIds ?? site.saltUserIds ?? false; + const nextPersistentClientIds = input.persistentClientIds ?? site.persistentClientIds ?? false; + + if (nextSaltUserIds && nextPersistentClientIds) { + throw new SiteLifecycleError( + "salting_persistent_id_conflict", + 400, + "User ID salting and persistent client IDs cannot both be enabled: disable one before enabling the other" + ); + } +} + function validateSiteId(siteId: number): void { if (!Number.isInteger(siteId) || siteId <= 0) { throw new SiteLifecycleError("invalid_site_id", 400, "Invalid site ID: must be a positive integer"); @@ -287,6 +310,7 @@ class SiteConfigurationLifecycle { validateSiteIdentity(nextSiteType, domain); } validateMobileFeatures(nextSiteType, input); + validateIdentitySettings(site, input); if (IS_CLOUD && input.sessionReplay === true) { const subscription = site.organizationId ? await getSubscriptionInner(site.organizationId) : null; diff --git a/server/src/services/tracker/identifyService.ts b/server/src/services/tracker/identifyService.ts index 143a27a8b..ebe33c226 100644 --- a/server/src/services/tracker/identifyService.ts +++ b/server/src/services/tracker/identifyService.ts @@ -96,13 +96,16 @@ export async function handleIdentify(request: FastifyRequest, reply: FastifyRepl const siteId = siteConfiguration.siteId; - const anonymousId = anonymous_id - ? await userIdService.generateUserIdFromClientId(anonymous_id, siteId) - : await userIdService.generateUserId( - ip_address || resolveClientIp(request, { firstPartyProxy: siteConfiguration.firstPartyProxy }), - user_agent || request.headers["user-agent"] || "", - siteId - ); + // Only honor a client-supplied anonymous_id when the site has opted into + // persistent client IDs — see createBasePayload for why. + const anonymousId = + siteConfiguration.persistentClientIds && anonymous_id + ? await userIdService.generateUserIdFromClientId(anonymous_id, siteId) + : await userIdService.generateUserId( + ip_address || resolveClientIp(request, { firstPartyProxy: siteConfiguration.firstPartyProxy }), + user_agent || request.headers["user-agent"] || "", + siteId + ); // Create alias if this is a new identify call (links anonymous_id to user_id) if (is_new_identify) { diff --git a/server/src/services/tracker/utils.test.ts b/server/src/services/tracker/utils.test.ts new file mode 100644 index 000000000..01e0654ac --- /dev/null +++ b/server/src/services/tracker/utils.test.ts @@ -0,0 +1,107 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { FastifyRequest } from "fastify"; +import type { SiteConfigData } from "../../lib/siteConfig.js"; + +const mocks = vi.hoisted(() => ({ + generateUserId: vi.fn(), + generateUserIdFromClientId: vi.fn(), + resolveTrackingIdentity: vi.fn(), +})); + +vi.mock("../userId/userIdService.js", () => ({ + userIdService: { + generateUserId: mocks.generateUserId, + generateUserIdFromClientId: mocks.generateUserIdFromClientId, + }, +})); + +vi.mock("./requestIdentity.js", () => ({ + resolveTrackingIdentity: mocks.resolveTrackingIdentity, +})); + +import { createBasePayload } from "./utils.js"; + +function baseSiteConfig(overrides: Partial = {}): SiteConfigData { + return { + id: "abc123", + siteId: 42, + type: "web", + public: false, + embedEnabled: false, + saltUserIds: false, + domain: "example.com", + blockBots: true, + firstPartyProxy: false, + persistentClientIds: false, + excludedIPs: [], + excludedCountries: [], + excludedPaths: [], + excludedHostnames: [], + excludedUserAgents: [], + sessionReplay: false, + webVitals: false, + trackErrors: false, + trackOutbound: true, + trackUrlParams: true, + trackInitialPageView: true, + trackSpaNavigation: true, + trackIp: false, + trackButtonClicks: false, + trackCopy: false, + trackFormInteractions: false, + tags: [], + ...overrides, + }; +} + +describe("createBasePayload anonymous_id gating", () => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.resolveTrackingIdentity.mockReturnValue({ + ipAddress: "203.0.113.10", + userAgent: "Mozilla/5.0", + candidateIps: ["203.0.113.10"], + }); + mocks.generateUserId.mockResolvedValue("fingerprint-abc"); + mocks.generateUserIdFromClientId.mockResolvedValue("client-id-xyz"); + }); + + it("ignores a client-supplied anonymous_id when persistentClientIds is off", async () => { + const payload = await createBasePayload( + {} as FastifyRequest, + "pageview", + { type: "pageview", site_id: "42", anonymous_id: "spoofed-visitor" } as any, + baseSiteConfig({ persistentClientIds: false }) + ); + + expect(mocks.generateUserIdFromClientId).not.toHaveBeenCalled(); + expect(mocks.generateUserId).toHaveBeenCalledWith("203.0.113.10", "Mozilla/5.0", 42); + expect(payload.userId).toBe("fingerprint-abc"); + }); + + it("honors the client-supplied anonymous_id when persistentClientIds is on", async () => { + const payload = await createBasePayload( + {} as FastifyRequest, + "pageview", + { type: "pageview", site_id: "42", anonymous_id: "visitor-real" } as any, + baseSiteConfig({ persistentClientIds: true }) + ); + + expect(mocks.generateUserIdFromClientId).toHaveBeenCalledWith("visitor-real", 42); + expect(mocks.generateUserId).not.toHaveBeenCalled(); + expect(payload.userId).toBe("client-id-xyz"); + }); + + it("falls back to the fingerprint when persistentClientIds is on but no anonymous_id is sent", async () => { + const payload = await createBasePayload( + {} as FastifyRequest, + "pageview", + { type: "pageview", site_id: "42" } as any, + baseSiteConfig({ persistentClientIds: true }) + ); + + expect(mocks.generateUserIdFromClientId).not.toHaveBeenCalled(); + expect(mocks.generateUserId).toHaveBeenCalledWith("203.0.113.10", "Mozilla/5.0", 42); + expect(payload.userId).toBe("fingerprint-abc"); + }); +}); diff --git a/server/src/services/tracker/utils.ts b/server/src/services/tracker/utils.ts index c20252b55..2927e5d1b 100644 --- a/server/src/services/tracker/utils.ts +++ b/server/src/services/tracker/utils.ts @@ -142,9 +142,13 @@ export async function createBasePayload( ); const { ip_address: _ipAddressOverride, user_agent: _userAgentOverride, ...payloadBody } = validatedBody; - const anonymousId = validatedBody.anonymous_id - ? await userIdService.generateUserIdFromClientId(validatedBody.anonymous_id, siteConfiguration.siteId) - : await userIdService.generateUserId(ipAddress, userAgent, siteConfiguration.siteId); + // Only honor a client-supplied anonymous_id when the site has opted into + // persistent client IDs — otherwise it's an unverified value from the + // browser and any visitor could claim to be any other visitor's identity. + const anonymousId = + siteConfiguration.persistentClientIds && validatedBody.anonymous_id + ? await userIdService.generateUserIdFromClientId(validatedBody.anonymous_id, siteConfiguration.siteId) + : await userIdService.generateUserId(ipAddress, userAgent, siteConfiguration.siteId); // userId is always the device fingerprint // identifiedUserId is the custom user ID when provided, empty string otherwise diff --git a/server/src/types/sessionReplay.ts b/server/src/types/sessionReplay.ts index ba4b61c03..8865359cb 100644 --- a/server/src/types/sessionReplay.ts +++ b/server/src/types/sessionReplay.ts @@ -47,6 +47,9 @@ export interface SessionReplayMetadata { export interface RecordSessionReplayRequest { userId: string; + // Client-side persistent identifier (localStorage), only honored when the + // site has opted into persistentClientIds; see sessionReplayIngestService. + anonymousId?: string; events: Array<{ type: string | number; data: any;