{"release_id":"01a1998d-9802-0000-b0f7-33a23307f4ed","chunk_id":"01a1998d-9914-70a0-a98f-5200ce2d560f","file":"Cs2lXbee.mjs","ignoreList":[],"mappings":"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","names":["framesOf","frames","fromOurBundles","f","isBrowserNoise","properties","list","first","dropBrowserNoise","cr","HANDOFF_DISTINCT_ID","HANDOFF_SESSION_ID","UUID_V7","MAX_SESSION_AGE_MS","mintedAt","uuid","parseHandoff","hash","now","params","distinctId","sessionId","age","arrived","receiveHandoff","url","clean","pixypilotHandoff","PUBLIC_POSTHOG_KEY","env","PUBLIC_POSTHOG_HOST","posthog","posthogInitialized","pendingOptIn","phQueue","enqueuePosthog","fn","initPromise","serverMintedDistinctId","m","hasPostHogIdentity","phKey","RAGECLICK","initPostHog","bootstrapFlags","__vitePreload","handoff","known","adopted","distinctID","hasFlags","enableTracking","resetTracking","trackAuthGateShown","p","event","trackRpGroupBuilder","step","props","trackAuthGateResumed","name","captureException","error","setUserProperties","setUserId","userId","trackPageView","path","trackFunnelStep","flow","extra","trackPhotoUpload","trackAlbumCardClick","albumId","surface","trackAlbumTagFilter","tag","active","source","trackAlbumMediaFilter","media","trackAlbumCreate","trackAlbumUpload","trackAlbumPublish","trackAlbumBundleTap","trackAlbumUnlockTap","trackModeSelect","mode","trackSavePhotoToggle","enabled","trackEmailSubmit","trackOtpVerify","trackOtpVerifyFailed","reason","trackAnalysisStart","trackAnalysisFail","trackAnalysisSettled","outcome","elapsedMs","trackUploadFail","trackScorecardDownload","score","trackLinkCopy","page","trackNukeResults","trackBreakdownUnlock","trackUpgradeModalOpen","feature","trackUpgradeModalClose","trackTargetedDuelBlocked","trackTargetedDuelExitOpen","trackDuelLimitBlocked","trackDuelLimitExit","to","trackPurchaseClick","tokens","price","pack","trackSingleUnlockView","generationId","trackSingleUnlockClick","trackSingleUnlockDismiss","trackCookieConsent","accepted","bar","trackCtaClick","cta","location","position","trackLanding","variant","trackShare","method","contentType","trackReelsOpen","trackReelView","postId","sourceType","trackReelToProfile","trackReelToChat","trackPostToChat","trackVideoLoadError","tile","trackImageLoadError","trackDuelCreate","code","format","trackDuelBoardViewed","trackDuelBoardCardClicked","trackDuelBoardCtaClicked","trackDuelLinkShared","shareChannel","isInviter","sharePosition","trackDuelImageShared","trackDuelCommunityModalOpen","trackDuelSubredditClick","subreddit","trackDuelChatComment","length","trackDuelChatReact","emoji","trackDuelEmojiPickerOpen","trackDuelLinkVisited","inviterUserId","referrer","utmSource","isFirstVisit","isAuthenticated","trackEnhanceStart","type","trackEnhanceComplete","trackCompareStart","trackCompareComplete","trackCompareFail","trackArenaEmote","emoteType","trackArenaSubmit","mediaType","trackArenaReveal","insightType","cost","trackArenaBoost","trackArenaConsent","trackArenaView","submissionId","dwellMs","trackBeatChallengeAccept","trackBeatChallengeAcceptLost","trackBeatVoteRejected","trackBeatLinkCopied","trackBeatRedditClick","trackBeatChallengeView","status","viewer","trackBeatListingView","counts","trackBeatListingScrolled50","trackRaterApplicationSubmit","style","trackRaterProfileViewed","opts","trackRaterSessionAbandoned","trackRaterFollowed","trackRaterRequestSkipped","trackRaterRequestCompleted","trackLivecamGreenroomView","trackLivecamPresetApplied","preset","tipCount","trackLivecamTipAdded","trackLivecamTipRemoved","trackLivecamGoLive","trackLivecamLiveStarted","trackLivecamGoLiveFailed","trackLivecamViewerStall","trackPeerRateOpened","peerRateId","trackPeerRateSubmitted","overall","trackPeerRateClosed","dirty","trackPeerRateRequestInitiated","trackPeerRateRequestSent","trackPeerRateRequestSuccess","trackPeerRateRequestFailed","trackChatComposerLocked","trackVideoAutoplayBlocked","trackVideoPosterFallback","trackFloorGetInLineTapped","trackFloorQueueJoined","trackTokenWallHit","trackBuyTokensModalOpen","trackSignInStart","trackSignInComplete","trackOAuthStart","provider","trackOAuthFailed","trackProfileView","trackFollowListView","trackProfileShareClick","trackPostDuelFollowCtaShown","trackNotificationDropdownOpened","trackNotificationClicked","trackRealtimeConnectionState","trackRoleplayMessagePullFailed","trackRoleplayMessageSent","chatId","trackRoleplayGenerationFailed","trackRoleplayBlocked","trackRoleplayQuotaHit","trackChallengeNotificationShown","trackChallengeNotificationClicked","trackRoadmapRailViewed","itemCount","trackRoadmapVote","feedbackId","voted","trackRoadmapRailBrowseClick","trackRoadmapRailCreateClick","trackFeedbackSubmitted","trackChatSendFailed","trackChatOpenerTapped","trackRoleplayWallHit","trackOfferShown","placement","products","trackOfferClicked","product","trackOfferDismissed","how","trackStudioClipViewed","trackVoiceSession","trackOnboardingChoice","choice","trackStudioPrompt","action","trackOnlineNowShown","count","trackOnlineNowTapped","replySeconds","isRater"],"sources":["../../../../../../src/lib/analytics-noise.ts","../../../../../../src/lib/pixypilot-handoff.ts","../../../../../../src/lib/analytics.ts"],"sourcesContent":["import type { CaptureResult } from 'posthog-js';\n\n/**\n * Exceptions that say nothing about this app, dropped before they're sent.\n *\n * Over the 30 days to 2026-09-27 these made up a large part of the client\n * $exception volume and of the \"errors\" column on PostHog's frustrating-pages\n * list, and none of them points at code of ours:\n *  - \"Script error.\" with no frames: ~20k events, 95% of them WebKit (iOS and\n *    macOS Safari, iOS Chrome and Firefox). The browser withheld everything\n *    but that string, because the throw came from a cross-origin script or an\n *    extension. There is nothing in it to act on.\n *  - \"Can't find variable: EmptyRanges\": iOS 26 Safari, no frames. A name that\n *    exists nowhere in this codebase — WebKit's own code.\n *  - A RangeError whose every frame sits in the page document itself (always\n *    \"line 226\", on every route, while the served HTML is ~100–150 lines): a\n *    script injected into iOS pages. This app's code runs from /_app/ bundles.\n */\n\ntype Frame = { filename?: unknown };\ntype ExceptionEntry = { type?: unknown; value?: unknown; stacktrace?: { frames?: unknown } | null };\n\nfunction framesOf(e: ExceptionEntry): Frame[] {\n\tconst frames = e?.stacktrace?.frames;\n\treturn Array.isArray(frames) ? (frames as Frame[]) : [];\n}\n\nconst fromOurBundles = (f: Frame) =>\n\ttypeof f.filename === 'string' && f.filename.includes('/_app/');\n\n/** True for an $exception's properties that match one of the cases above. */\nexport function isBrowserNoise(properties: Record<string, unknown> | undefined): boolean {\n\tconst list = properties?.$exception_list;\n\tif (!Array.isArray(list) || list.length === 0) return false;\n\tconst first = list[0] as ExceptionEntry;\n\tconst frames = framesOf(first);\n\tif (first.value === 'Script error.' && frames.length === 0) return true;\n\tif (first.type === 'ReferenceError' && first.value === \"Can't find variable: EmptyRanges\")\n\t\treturn true;\n\tif (first.type === 'RangeError' && frames.length > 0 && !frames.some(fromOurBundles)) return true;\n\treturn false;\n}\n\n/** posthog-js `before_send`: returning null drops the event. */\nexport function dropBrowserNoise(cr: CaptureResult | null): CaptureResult | null {\n\tif (cr?.event === '$exception' && isBrowserNoise(cr.properties)) return null;\n\treturn cr;\n}\n","/** A visitor sent on by pixypilot.app (RAT-550). Client-safe.\n *\n *  The campaigns land on pixypilot, whose last Continue leads here. Cookies and\n *  storage are per domain, so PostHog would see two strangers: one who watched\n *  the landing and one who signed up. The landing therefore puts its PostHog\n *  distinct id and session id in the fragment of the address it sends the\n *  visitor to, and initPostHog carries on under them when this browser has no\n *  PostHog identity of its own.\n *\n *  The ids are taken out of the address before anything else happens: a link\n *  copied with them in it would hand the next person this visitor's identity.\n *  Only what PostHog itself mints is accepted, a UUIDv7, and only from a session\n *  that began in the last half hour. A signed-in user's distinct id is their\n *  account id, a UUIDv4, so a made-up link cannot write events into someone's\n *  person, and an old link is worth nothing.\n */\n\n/** The fragment's two parameters. src/lib/server/pixypilot/index.html writes\n *  them by the same names; its test holds the two together. */\nexport const HANDOFF_DISTINCT_ID = 'pp_did';\nexport const HANDOFF_SESSION_ID = 'pp_sid';\n\nexport type PixypilotHandoff = { distinctId: string; sessionId: string };\n\nconst UUID_V7 = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\nconst MAX_SESSION_AGE_MS = 30 * 60 * 1000;\n/** The session's start is read off the visitor's own clock on both sides, so\n *  this only has to cover a clock that was set in between. */\nconst MAX_CLOCK_SKEW_MS = 5 * 60 * 1000;\n\n/** A UUIDv7 opens with the millisecond it was minted in. */\nfunction mintedAt(uuid: string): number {\n\treturn parseInt(uuid.replace(/-/g, '').slice(0, 12), 16);\n}\n\nexport function parseHandoff(hash: string, now = Date.now()): PixypilotHandoff | null {\n\tconst params = new URLSearchParams(hash.replace(/^#/, ''));\n\tconst distinctId = params.get(HANDOFF_DISTINCT_ID);\n\tconst sessionId = params.get(HANDOFF_SESSION_ID);\n\tif (!distinctId || !sessionId) return null;\n\tif (!UUID_V7.test(distinctId) || !UUID_V7.test(sessionId)) return null;\n\tconst age = now - mintedAt(sessionId);\n\tif (age > MAX_SESSION_AGE_MS || age < -MAX_CLOCK_SKEW_MS) return null;\n\treturn { distinctId, sessionId };\n}\n\nlet arrived: PixypilotHandoff | null = null;\n\n/** Reads the handoff off the address and keeps it for initPostHog. Returns the\n *  address without it, for the caller to put in the bar, or null when the\n *  fragment carries no handoff at all. */\nexport function receiveHandoff(url: URL): URL | null {\n\tconst params = new URLSearchParams(url.hash.replace(/^#/, ''));\n\tif (!params.has(HANDOFF_DISTINCT_ID) && !params.has(HANDOFF_SESSION_ID)) return null;\n\tarrived = parseHandoff(url.hash);\n\tparams.delete(HANDOFF_DISTINCT_ID);\n\tparams.delete(HANDOFF_SESSION_ID);\n\tconst clean = new URL(url);\n\tclean.hash = params.toString();\n\treturn clean;\n}\n\n/** The handoff this page load arrived with, if it was a valid one. */\nexport function pixypilotHandoff(): PixypilotHandoff | null {\n\treturn arrived;\n}\n","/**\n * Centralized analytics — PostHog only.\n *\n * The SDK (~187KB minified) is dynamically imported on first user interaction,\n * so it doesn't sit on the critical path of the homepage. Calls made before\n * the SDK loads are queued and replayed once `initPostHog()` resolves.\n *\n * GA4 was removed in favour of PostHog-only telemetry.\n */\n\nimport { browser } from '$app/environment';\nimport { env } from '$env/dynamic/public';\nimport type { PostHog, RageclickConfig } from 'posthog-js';\nimport type { ShareChannel } from '$lib/types/duel';\nimport { dropBrowserNoise } from '$lib/analytics-noise';\nimport { pixypilotHandoff } from '$lib/pixypilot-handoff';\n\nconst PUBLIC_POSTHOG_KEY = env.PUBLIC_POSTHOG_KEY;\nconst PUBLIC_POSTHOG_HOST = env.PUBLIC_POSTHOG_HOST;\n\nlet posthog: PostHog | null = null;\nlet posthogInitialized = false;\nlet pendingOptIn = false;\nconst phQueue: Array<(p: PostHog) => void> = [];\n\nfunction enqueuePosthog(fn: (p: PostHog) => void) {\n\tif (posthog) fn(posthog);\n\telse phQueue.push(fn);\n}\n\nlet initPromise: Promise<void> | null = null;\n\n/**\n * The distinct id the server bucketed this visitor under, when posthog-js has\n * none of its own yet. Server-side flag evaluation ($lib/server/flags.ts) mints\n * an `rmd_did` cookie for a first-paint anonymous visitor; unless posthog-js\n * adopts it, the exposure logged under that id is never joined to the visitor's\n * own events or to their signup, and the experiment can't attribute them.\n *\n * Only when no PostHog identity is stored (see hasPostHogIdentity).\n */\nfunction serverMintedDistinctId(): string | undefined {\n\tconst m = document.cookie.match(/(?:^|;\\s*)rmd_did=([^;]+)/);\n\treturn m ? decodeURIComponent(m[1]) : undefined;\n}\n\n/**\n * Whether posthog-js already knows this browser. An id handed to it from\n * outside — the server's, or pixypilot's — may only be bootstrapped when it\n * doesn't: a bootstrap distinctID that isn't marked identified overwrites\n * whatever id posthog-js has, an identified user's included (posthog-js 1.372,\n * the init bootstrap branch). Storage that can't be read counts as an identity.\n */\nfunction hasPostHogIdentity(): boolean {\n\tconst phKey = `ph_${PUBLIC_POSTHOG_KEY}_posthog`;\n\tif (document.cookie.includes(`${phKey}=`)) return true;\n\ttry {\n\t\treturn localStorage.getItem(phKey) !== null;\n\t} catch {\n\t\treturn true;\n\t}\n}\n\n/**\n * Rage clicks, minus the repeated clicks that aren't frustration. PostHog calls\n * any three clicks within 30px and a second a rage click, whatever they land\n * on, and over the 30 days to 2026-09-27 about a third of ours weren't:\n * clicks in a text field while writing — placing the caret, selecting a word\n * — were 28% of all of them, and 97% of the sessions that \"rage-clicked\" the\n * DM composer sent a message within two minutes. posthog-js leaves text fields\n * out by itself only from the '2026-05-30' defaults, which 1.372 predates, so\n * they're listed here. A list here replaces PostHog's default one, hence\n * .ph-no-rageclick (fast-repeat controls — the arena vote buttons, ± steppers\n * — carry that class); content_ignorelist: true keeps the next/previous\n * keywords that the '2026-01-30' defaults switch on.\n */\nconst RAGECLICK: RageclickConfig = {\n\tcss_selector_ignorelist: [\n\t\t'.ph-no-rageclick',\n\t\t'textarea',\n\t\t'[contenteditable]:not([contenteditable=\"false\"])',\n\t\t'input:not([type])',\n\t\t'input[type=\"text\"]',\n\t\t'input[type=\"search\"]',\n\t\t'input[type=\"email\"]',\n\t\t'input[type=\"url\"]',\n\t\t'input[type=\"tel\"]',\n\t\t'input[type=\"password\"]',\n\t\t'input[type=\"number\"]'\n\t],\n\tcontent_ignorelist: true\n};\n\n/**\n * Idempotent PostHog loader. Called by LazyAnalytics on first user interaction.\n * Safe to call multiple times — subsequent calls return the same in-flight promise.\n *\n * `bootstrapFlags` are flag values the page evaluated server-side, so events\n * captured before posthog-js's own /flags call returns already carry them.\n */\nexport function initPostHog(bootstrapFlags: Record<string, string | boolean> = {}): Promise<void> {\n\tif (!browser || !PUBLIC_POSTHOG_KEY || !PUBLIC_POSTHOG_HOST) return Promise.resolve();\n\tif (initPromise) return initPromise;\n\tinitPromise = (async () => {\n\t\ttry {\n\t\t\tconst mod = await import('posthog-js');\n\t\t\tposthog = mod.default;\n\t\t\t// RAT-550: a visitor sent on by pixypilot.app carries on under the ids\n\t\t\t// they had there, session included, so the campaign's click and a\n\t\t\t// sign-up here are one person in one session. It goes before the\n\t\t\t// server's id: pixypilot's has the landing's events behind it.\n\t\t\tconst handoff = pixypilotHandoff();\n\t\t\tconst known = hasPostHogIdentity();\n\t\t\tconst adopted = !known && handoff !== null;\n\t\t\tconst distinctID = known ? undefined : (handoff?.distinctId ?? serverMintedDistinctId());\n\t\t\tconst hasFlags = Object.keys(bootstrapFlags).length > 0;\n\t\t\tposthog.init(PUBLIC_POSTHOG_KEY, {\n\t\t\t\t...(distinctID || hasFlags\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tbootstrap: {\n\t\t\t\t\t\t\t\t...(distinctID ? { distinctID } : {}),\n\t\t\t\t\t\t\t\t...(adopted ? { sessionID: handoff.sessionId } : {}),\n\t\t\t\t\t\t\t\t...(hasFlags ? { featureFlags: bootstrapFlags } : {})\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t: {}),\n\t\t\t\tapi_host: PUBLIC_POSTHOG_HOST,\n\t\t\t\tdefaults: '2026-01-30',\n\t\t\t\tcapture_pageview: 'history_change',\n\t\t\t\tcapture_exceptions: true,\n\t\t\t\t// Web vitals with attribution: which element shifted, which one was the\n\t\t\t\t// largest paint. The studio's mobile CLS sits at 0.398 p75 and every\n\t\t\t\t// $web_vitals event so far carried `entries` as empty objects, so there\n\t\t\t\t// was no way to see what moved. network_timing is left to the project's\n\t\t\t\t// remote config, as it was.\n\t\t\t\tcapture_performance: { web_vitals: true, web_vitals_attribution: true },\n\t\t\t\trageclick: RAGECLICK,\n\t\t\t\t// Browser-internal and injected-script exceptions (see analytics-noise).\n\t\t\t\tbefore_send: dropBrowserNoise,\n\t\t\t\topt_out_capturing_by_default: false,\n\t\t\t\tperson_profiles: 'identified_only'\n\t\t\t});\n\t\t\tposthogInitialized = true;\n\t\t\tif (pendingOptIn) posthog.opt_in_capturing();\n\t\t\t// Sent whether or not the ids were taken over: a visitor who already had\n\t\t\t// an identity here keeps it, and pixypilot's ids on the event are then\n\t\t\t// the only thing joining the two halves.\n\t\t\tif (handoff) {\n\t\t\t\tposthog.capture('pixypilot_arrival', {\n\t\t\t\t\tadopted,\n\t\t\t\t\tpixypilot_distinct_id: handoff.distinctId,\n\t\t\t\t\tpixypilot_session_id: handoff.sessionId\n\t\t\t\t});\n\t\t\t}\n\t\t\tfor (const fn of phQueue) fn(posthog);\n\t\t\tphQueue.length = 0;\n\t\t} catch {\n\t\t\t// Network blip / chunk fetch fail — analytics silently disabled this session\n\t\t}\n\t})();\n\treturn initPromise;\n}\n\n/** Called by CookieConsent on accept — flips PostHog from opt-out to opt-in. */\nexport function enableTracking() {\n\tif (!browser) return;\n\tif (posthogInitialized && posthog) {\n\t\tposthog.opt_in_capturing();\n\t} else {\n\t\tpendingOptIn = true;\n\t}\n}\n\n/** Called on logout — clears PostHog identification so the next user isn't merged. */\nexport function resetTracking() {\n\tif (browser && posthogInitialized && posthog) posthog.reset();\n}\n\n/** RAT-519: a signed-out visitor hit a sign-in gate; the sheet opened. */\nexport function trackAuthGateShown(p: { surface: string }) {\n\tevent('auth_gate_shown', { surface: p.surface });\n}\n\n/** Group roleplay builder: opened → scene step done → cast picked. The room\n *  creation and first message are captured server-side. */\nexport function trackRpGroupBuilder(step: 'opened' | 'scene_done' | 'cast_done', props: Record<string, unknown> = {}) {\n\tevent(`rp_group_builder_${step}`, props);\n}\n\n/** The action a gate stood in front of was carried out after sign-in. */\nexport function trackAuthGateResumed(p: { surface: string }) {\n\tevent('auth_gate_resumed', { surface: p.surface });\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction event(name: string, params?: Record<string, any>) {\n\tif (browser) enqueuePosthog((p) => p.capture(name, params));\n}\n\n/** An exception, queued like any other call until the SDK has loaded, so one\n *  from before that (a route chunk failing mid-hydration) still arrives. */\nexport function captureException(error: unknown, properties?: Record<string, unknown>) {\n\tif (browser) enqueuePosthog((p) => p.captureException(error, properties));\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function setUserProperties(props: Record<string, any>) {\n\tif (browser) enqueuePosthog((p) => p.setPersonProperties(props));\n}\n\nexport function setUserId(userId: string) {\n\tif (browser) enqueuePosthog((p) => p.identify(userId));\n}\n\n// ──────────────────────────────────────────\n// Page views (SPA navigation)\n// ──────────────────────────────────────────\n\nexport function trackPageView(path: string) {\n\tif (browser) enqueuePosthog((p) => p.capture('$pageview', { path }));\n}\n\n// ──────────────────────────────────────────\n// Funnel events  (rate / enhance / duel)\n// ──────────────────────────────────────────\n\nexport function trackFunnelStep(\n\tstep: string,\n\tflow: string,\n\textra?: Record<string, string | number | boolean>\n) {\n\tevent('funnel_step', { step, flow, ...extra });\n}\n\nexport function trackPhotoUpload(flow: string) {\n\tevent('photo_upload', { flow });\n}\n\n/** RAT-449: album card clicked on a tracked surface (feed/browse/arena). The\n *  DB-side funnel is the ranking's source of truth; this mirrors it into\n *  PostHog for dashboards. */\nexport function trackAlbumCardClick(albumId: string, surface: string) {\n\tevent('album_card_click', { album_id: albumId, surface });\n}\n\n/** Album browse tag filter toggled — tag-level demand signal (which tags get\n *  filtered on vs which have inventory drives creator recruiting). */\nexport function trackAlbumTagFilter(tag: string, active: boolean, source: 'chips' | 'picker') {\n\tevent('album_tag_filter', { tag, active, source });\n}\n\n/** RAT-621: the album browser's all / videos / photos switch. */\nexport function trackAlbumMediaFilter(media: string, surface: 'albums' | 'hub') {\n\tevent('album_media_filter', { media, surface });\n}\n\n/** The album studio's \"new album\" button. `entry` is the page that brought\n *  the creator to the studio (its ?from=, else 'direct'). Until this event\n *  the creation funnel had no step in PostHog before the purchase. */\nexport function trackAlbumCreate(p: { entry: string; firstAlbum: boolean }) {\n\tevent('album_create', { entry: p.entry, first_album: p.firstAlbum });\n}\n\n/** One batch of files picked in the album editor, once it has settled.\n *  `reasons` lists why files failed (too_large, unsupported, too_long, full,\n *  network, storage, server), one entry per failed file. */\nexport function trackAlbumUpload(p: {\n\tfiles: number;\n\tok: number;\n\tfailed: number;\n\treasons: string[];\n\tms: number;\n\tfirst: boolean;\n\tvideos: number;\n}) {\n\tevent('album_upload', {\n\t\tfiles: p.files,\n\t\tok: p.ok,\n\t\tfailed: p.failed,\n\t\treasons: p.reasons,\n\t\tms: p.ms,\n\t\tfirst: p.first,\n\t\tvideos: p.videos\n\t});\n}\n\n/** An album went live from the editor (first publish or re-publish). */\nexport function trackAlbumPublish(p: {\n\talbumId: string;\n\titems: number;\n\tpreviews: number;\n\tvideos: number;\n\tpriceTokens: number;\n\tdescLen: number;\n}) {\n\tevent('album_publish', {\n\t\talbum_id: p.albumId,\n\t\titems: p.items,\n\t\tpreviews: p.previews,\n\t\tvideos: p.videos,\n\t\tprice_tokens: p.priceTokens,\n\t\tdesc_len: p.descLen\n\t});\n}\n\n/** The \"all N albums by this creator\" offer was tapped on an album page. The\n *  purchase itself is the server's album_unlocked, once per album, with\n *  bundle: true. */\nexport function trackAlbumBundleTap(p: {\n\talbumId: string;\n\tcount: number;\n\tpriceTokens: number;\n\tcanAfford: boolean;\n}) {\n\tevent('album_bundle_tap', {\n\t\talbum_id: p.albumId,\n\t\tcount: p.count,\n\t\tprice_tokens: p.priceTokens,\n\t\tcan_afford: p.canAfford\n\t});\n}\n\n/** RAT-517: someone pressed unlock on an album page, before anything happens —\n *  the sign-in, the token sheet or the purchase. `surface` is the buy box or the\n *  phone's sticky bar; the purchase itself is the server's album_unlocked. */\nexport function trackAlbumUnlockTap(p: {\n\talbumId: string;\n\t/** `player`: the button under the free preview clip (RAT-618) */\n\tsurface: 'box' | 'bar' | 'player';\n\tsignedIn: boolean;\n\tcanAfford: boolean;\n}) {\n\tevent('album_unlock_tap', {\n\t\talbum_id: p.albumId,\n\t\tsurface: p.surface,\n\t\tsigned_in: p.signedIn,\n\t\tcan_afford: p.canAfford\n\t});\n}\n\nexport function trackModeSelect(mode: string, flow: string) {\n\tevent('mode_select', { mode, flow });\n}\n\nexport function trackSavePhotoToggle(enabled: boolean) {\n\tevent('save_photo_toggle', { enabled });\n}\n\nexport function trackEmailSubmit(flow: string) {\n\tevent('email_submit', { flow });\n}\n\nexport function trackOtpVerify(flow: string) {\n\tevent('otp_verify', { flow });\n}\n\n/** Failed OTP verification. Deliberately NOT `otp_verify` with a success\n *  flag: `otp_verify` has only ever fired after a successful verify on\n *  every surface (/rate, /d, /a, the auth sheet), so overloading it would\n *  silently inflate every existing insight built on it. */\nexport function trackOtpVerifyFailed(flow: string, reason: string) {\n\tevent('otp_verify_failed', { flow, reason });\n}\n\n// ──────────────────────────────────────────\n// Analysis events\n// ──────────────────────────────────────────\n\nexport function trackAnalysisStart(mode: string, flow: string) {\n\tevent('analysis_start', { mode, flow });\n}\n\n// RAT-207: trackAnalysisComplete moved server-side to api/generation/[id]/+server.ts.\n\nexport function trackAnalysisFail(reason: string, flow: string) {\n\tevent('analysis_fail', { reason, flow });\n}\n\n/**\n * RAT-462: client-side TERMINAL event for the /rate analyze loader. It fires\n * exactly once, the moment the loader reaches an end state.\n *\n * Why it's needed: `analysis_complete` is captured server-side (no\n * `$session_id`) and the failure paths only emit `analysis_fail`, so a loader\n * that NEVER resolves — a killed/OOM'd serverless function leaves the row stuck\n * `processing` and the ring frozen at 92% — was completely invisible in\n * analytics. With this, \"started but the loader never settled\" is measurable as\n * a session with `analysis_start` and no `analysis_settled`.\n *\n * `outcome` splits the resolution: `completed` (result shown), `failed` (server\n * or POST error), `timeout` (client-side wall-clock deadline hit — the stuck\n * `processing` case). `elapsed_ms` is wall-clock from loader mount to settle.\n */\nexport function trackAnalysisSettled(\n\toutcome: 'completed' | 'failed' | 'timeout',\n\tflow: string,\n\telapsedMs: number\n) {\n\tevent('analysis_settled', { outcome, flow, elapsed_ms: elapsedMs });\n}\n\n/**\n * The client-side Supabase Storage upload on /rate failed. This upload gates the\n * whole get-rated funnel (upload → email/OTP signup), so a miss here hard-blocks\n * the signup. It used to be only `console.error`'d — with `analysis_fail` firing\n * on just one of the four upload call sites — so we were blind to how often it\n * happened. This fires on EVERY failure branch with the underlying error string\n * so the blast radius is measurable.\n *\n * Fires exactly ONCE per user-facing failure, and REPLACES the old\n * `analysis_fail(reason: 'upload_failed')` on the logged-in path — don't add\n * that one back alongside it or the same failure gets counted twice.\n *\n * `context` splits which entry point failed (logged_in / pre_upload / email /\n * oauth). `stale` flags a dead File handle (`NotReadableError` and friends —\n * the iOS-Safari gotcha where a picked File is held across a long login flow).\n * `handleFile` now detaches picked files from their OS handle, so `stale` should\n * read false; if it starts showing up, that detach isn't holding.\n */\nexport function trackUploadFail(props: { error: string; context: string; stale: boolean }) {\n\tevent('upload_fail', {\n\t\terror: props.error,\n\t\tcontext: props.context,\n\t\tstale: props.stale\n\t});\n}\n\n// ──────────────────────────────────────────\n// Result page actions\n// ──────────────────────────────────────────\n\nexport function trackScorecardDownload(score: number, mode: string) {\n\tevent('scorecard_download', { score, mode });\n}\n\nexport function trackLinkCopy(page: string) {\n\tevent('link_copy', { page });\n}\n\nexport function trackNukeResults() {\n\tevent('nuke_results');\n}\n\n// ──────────────────────────────────────────\n// Breakdown unlock\n// ──────────────────────────────────────────\n\nexport function trackBreakdownUnlock(score: number, mode: string) {\n\tevent('breakdown_unlock', { score, mode });\n}\n\n// ──────────────────────────────────────────\n// Upgrade / purchase\n// ──────────────────────────────────────────\n\nexport function trackUpgradeModalOpen(feature: string | null) {\n\tevent('upgrade_modal_open', { feature: feature || 'general' });\n}\n\nexport function trackUpgradeModalClose() {\n\tevent('upgrade_modal_close');\n}\n\n/** Free targeted-duel allowance spent. `surface` separates the wall the user\n *  actually meets (chat_menu) from the server's defensive rejection at submit\n *  (create_action) — a rising create_action share means the client gate leaked. */\nexport function trackTargetedDuelBlocked(props: {\n\tused: number | null;\n\tlimit: number | null;\n\tsurface: string;\n}) {\n\tevent('duel_targeted_quota_blocked', props);\n}\n\n/** Took the free way out of the targeted wall — an open duel anyone can accept.\n *  This, not the subscribe, is the outcome the cap is designed to produce, so it\n *  is measured on its own rather than folded into the upgrade funnel. */\nexport function trackTargetedDuelExitOpen(surface: string) {\n\tevent('duel_targeted_quota_exit_open', { surface });\n}\n\n/** RAT-569: the lifetime free-duel allowance is spent and the wall was shown.\n *  `surface` is where (duel_page, rate_result, chat_menu). Same event name as\n *  the server's backstop record (duel_limit_blocked), told apart by surface. */\nexport function trackDuelLimitBlocked(props: {\n\tused: number | null;\n\tlimit: number | null;\n\tsurface: string;\n}) {\n\tevent('duel_limit_blocked', props);\n}\n\n/** Took the free way out of the free-duel wall (join an open duel, or a beat me\n *  in chat) — measured apart from the subscribe path so the cap's cost to the\n *  loop can be read off directly. */\nexport function trackDuelLimitExit(surface: string, to: 'open_duels' | 'beat') {\n\tevent('duel_limit_exit', { surface, to });\n}\n\nexport function trackPurchaseClick(tokens: number, price: string, pack: string) {\n\tevent('begin_checkout', {\n\t\tcurrency: 'USD',\n\t\tvalue: parseFloat(price.replace('$', '')),\n\t\titems: [{ item_name: pack, quantity: 1 }]\n\t});\n\tevent('purchase_click', { tokens, price, pack });\n}\n\n// ──────────────────────────────────────────\n// Single-unlock (loader paywall, $1.99)\n// ──────────────────────────────────────────\n\nexport function trackSingleUnlockView(generationId: string) {\n\tevent('single_unlock_view', { generation_id: generationId });\n}\n\nexport function trackSingleUnlockClick(generationId: string) {\n\tevent('begin_checkout', {\n\t\tcurrency: 'USD',\n\t\tvalue: 1.99,\n\t\titems: [{ item_name: 'single_unlock', quantity: 1 }]\n\t});\n\tevent('single_unlock_click', { generation_id: generationId });\n}\n\nexport function trackSingleUnlockDismiss(generationId: string) {\n\tevent('single_unlock_dismiss', { generation_id: generationId });\n}\n\n// ──────────────────────────────────────────\n// Engagement\n// ──────────────────────────────────────────\n\n/** `bar`: which notice was answered (RAT-524) — the card, or the slim line a\n *  phone in the `consent-bar-slim` test gets. */\nexport function trackCookieConsent(accepted: boolean, bar?: 'card' | 'slim') {\n\tevent('cookie_consent', { accepted, ...(bar ? { bar } : {}) });\n}\n\nexport function trackCtaClick(cta: string, location: string, position?: number) {\n\tevent('cta_click', { cta, location, position });\n}\n\n// ──────────────────────────────────────────\n// Homepage landing funnel (the landing-product-strip test's events, kept after\n// it ended with C + strip, RAT-555)\n// ──────────────────────────────────────────\n\n/** Fired identically from both landings so the funnels compare. Every event\n *  carries `variant` explicitly: it's set before posthog-js has loaded its own\n *  flags, and it's what the breakdowns split on. */\nexport type LandingEvent =\n\t| 'landing_viewed'\n\t| 'landing_cta_clicked'\n\t| 'landing_secondary_clicked'\n\t| 'landing_route_picked'\n\t| 'landing_faq_opened'\n\t| 'landing_pricing_viewed'\n\t| 'landing_plan_clicked'\n\t| 'landing_pack_clicked';\n\nexport function trackLanding(\n\tname: LandingEvent,\n\tvariant: string,\n\tprops: Record<string, string | number> = {}\n) {\n\tevent(name, { variant, ...props });\n}\n\nexport function trackShare(method: string, contentType: string) {\n\tevent('share', { method, content_type: contentType });\n}\n\n// ──────────────────────────────────────────\n// Reels (RAT-446)\n// ──────────────────────────────────────────\n\nexport function trackReelsOpen(source: string) {\n\tevent('reels_open', { source });\n}\n\nexport function trackReelView(postId: string, sourceType: string | null) {\n\tevent('reel_view', { post_id: postId, source_type: sourceType });\n}\n\nexport function trackReelToProfile(postId: string) {\n\tevent('reel_to_profile', { post_id: postId });\n}\n\nexport function trackReelToChat(postId: string) {\n\tevent('reel_to_chat', { post_id: postId });\n}\n\n/** RAT-523: the post card's message button — reel_to_chat's twin in the feed. */\nexport function trackPostToChat(postId: string) {\n\tevent('post_to_chat', { post_id: postId });\n}\n\n// ──────────────────────────────────────────\n// Media playback failures (RAT: video spins forever)\n//\n// Both in-app lightbox players used to swallow a stalled / dead video onto an\n// endless spinner with no `$exception` and no capture, so we were blind on how\n// often it happened. This fires once per failed load so the rate is measurable —\n// `surface` splits chat vs the monetized album preview, `reason` splits a hard\n// error from a load timeout (silent stall), and `tile` splits WHICH player blew\n// up.\n//\n// `tile` is required, not optional. The inline chat tiles were wired into this\n// event after the fact, and without a discriminator (a) one broken clip lands in\n// the series twice — inline tile on scroll, then lightbox on tap — and (b) the\n// historical `surface='chat'` numbers, which meant \"the lightbox player failed\"\n// and nothing else, silently change meaning. Filter `tile='lightbox'` to read\n// the old series forward.\n// ──────────────────────────────────────────\n\nexport type VideoErrorTile = 'inline_poster' | 'inline_video' | 'lightbox';\n\nexport function trackVideoLoadError(\n\tsurface: 'chat' | 'album' | 'story',\n\treason: 'error' | 'timeout',\n\ttile: VideoErrorTile\n) {\n\tevent('video_load_error', { surface, reason, tile });\n}\n\n// Image load failures (post / profile media). `ImageWithShimmer` used to sit on\n// a perpetual pulsing shimmer when an <img> failed — no onerror, no capture, so\n// broken images (mostly expired signed URLs mid-session) were invisible outside\n// session replay. This fires once per failed load; `surface` tags where it\n// happened so the rate is measurable. A closed union, like trackVideoLoadError's\n// — a free-form string here would let typos quietly fragment the series.\nexport type ImageErrorSurface = 'post_feed' | 'post_detail' | 'link_card';\n\nexport function trackImageLoadError(surface: ImageErrorSurface) {\n\tevent('image_load_error', { surface });\n}\n\n// ──────────────────────────────────────────\n// Duel events\n// ──────────────────────────────────────────\n\nexport function trackDuelCreate(mode: string, code: string, format: string = '1v1') {\n\tevent('duel_create', { mode, format, code });\n}\n\n/** RAT-518: the /d board rendered, once per page view, in whichever arm of the\n *  switchback was live. `waiting_total` is what a free viewer's CTA counts. */\nexport function trackDuelBoardViewed(p: {\n\tarm: 'control' | 'waiting';\n\tfeatured: number;\n\twaiting: number;\n\twaitingTotal: number;\n\tfull: boolean;\n}) {\n\tevent('duel_board_viewed', {\n\t\tarm: p.arm,\n\t\tfeatured_count: p.featured,\n\t\twaiting_count: p.waiting,\n\t\twaiting_total: p.waitingTotal,\n\t\tfull: p.full\n\t});\n}\n\n/** A duel on the board was opened. `position` is 0-based, featured rows first. */\nexport function trackDuelBoardCardClicked(p: {\n\tfeatured: boolean;\n\tposition: number;\n\tformat: string;\n\tfull: boolean;\n}) {\n\tevent('duel_board_card_clicked', {\n\t\tfeatured: p.featured,\n\t\tposition: p.position,\n\t\tformat: p.format,\n\t\tfull: p.full\n\t});\n}\n\n/** The free viewer's \"all N with Pro\" was pressed. */\nexport function trackDuelBoardCtaClicked(p: { waitingTotal: number; signedIn: boolean }) {\n\tevent('duel_board_cta_clicked', { waiting_total: p.waitingTotal, signed_in: p.signedIn });\n}\n\n// RAT-207: trackDuelComplete moved server-side to lib/server/duel.ts (finalizeReveal).\n\nexport function trackDuelLinkShared(\n\tcode: string,\n\tshareChannel: ShareChannel,\n\tisInviter: boolean,\n\tsharePosition: 'post_create' | 'duel_page' | 'result_page'\n) {\n\tevent('duel_link_shared', {\n\t\tcode,\n\t\tshare_channel: shareChannel,\n\t\tis_inviter: isInviter,\n\t\tshare_position: sharePosition\n\t});\n}\n\n/**\n * Image-share is a meaningfully different funnel from link-share — Reddit feed\n * thumbnails, Discord image embeds, IG story uploads — so it gets its own event.\n * `outcome` separates the native-share-sheet path (mobile, lands directly in a\n * target app) from the desktop blob-download path (user has to attach manually).\n */\nexport function trackDuelImageShared(\n\tcode: string,\n\toutcome: 'native_share' | 'download',\n\tisInviter: boolean,\n\tsharePosition: 'post_create' | 'duel_page' | 'result_page'\n) {\n\tevent('duel_image_shared', {\n\t\tcode,\n\t\toutcome,\n\t\tis_inviter: isInviter,\n\t\tshare_position: sharePosition\n\t});\n}\n\nexport function trackDuelCommunityModalOpen(\n\tcode: string,\n\tisInviter: boolean,\n\tsharePosition: 'post_create' | 'duel_page' | 'result_page'\n) {\n\tevent('duel_community_modal_open', {\n\t\tcode,\n\t\tis_inviter: isInviter,\n\t\tshare_position: sharePosition\n\t});\n}\n\nexport function trackDuelSubredditClick(\n\tcode: string,\n\tsubreddit: string,\n\tisInviter: boolean,\n\tsharePosition: 'post_create' | 'duel_page' | 'result_page'\n) {\n\tevent('duel_subreddit_click', {\n\t\tcode,\n\t\tsubreddit,\n\t\tis_inviter: isInviter,\n\t\tshare_position: sharePosition\n\t});\n}\n\nexport function trackDuelChatComment(code: string, length: number) {\n\tevent('duel_chat_comment', { code, length });\n}\n\nexport function trackDuelChatReact(code: string, emoji: string) {\n\tevent('duel_chat_react', { code, emoji });\n}\n\nexport function trackDuelEmojiPickerOpen(code: string) {\n\tevent('duel_emoji_picker_open', { code });\n}\n\nexport function trackDuelLinkVisited(\n\tcode: string,\n\tinviterUserId: string | null,\n\treferrer: string | null,\n\tutmSource: string | null,\n\tisFirstVisit: boolean,\n\tisAuthenticated: boolean\n) {\n\tevent('duel_link_visited', {\n\t\tcode,\n\t\tinviter_user_id: inviterUserId,\n\t\treferrer,\n\t\tutm_source: utmSource,\n\t\tis_first_visit: isFirstVisit,\n\t\tis_authenticated: isAuthenticated\n\t});\n}\n\n// ──────────────────────────────────────────\n// Enhance events\n// ──────────────────────────────────────────\n\nexport function trackEnhanceStart(type: string) {\n\tevent('enhance_start', { type });\n}\n\nexport function trackEnhanceComplete(type: string) {\n\tevent('enhance_complete', { type });\n}\n\n// ──────────────────────────────────────────\n// Compare events\n// ──────────────────────────────────────────\n\nexport function trackCompareStart() {\n\tevent('compare_start', { image_count: 2 });\n}\n\nexport function trackCompareComplete() {\n\tevent('compare_complete', { image_count: 2 });\n}\n\nexport function trackCompareFail(reason: string) {\n\tevent('compare_fail', { reason, image_count: 2 });\n}\n\n// ──────────────────────────────────────────\n// Arena events\n// ──────────────────────────────────────────\n\n// RAT-207: trackArenaSwipe moved server-side to api/arena/vote/+server.ts.\n\nexport function trackArenaEmote(emoteType: string) {\n\tevent('arena_emote', { emote_type: emoteType });\n}\n\nexport function trackArenaSubmit(mediaType: 'image' | 'video' = 'image') {\n\tevent('arena_submit', { media_type: mediaType });\n}\n\nexport function trackArenaReveal(insightType: string, cost: number) {\n\tevent('arena_reveal', { insight_type: insightType, cost });\n}\n\nexport function trackArenaBoost() {\n\tevent('arena_boost');\n}\n\nexport function trackArenaConsent() {\n\tevent('arena_consent_accept');\n}\n\nexport function trackArenaView(\n\tsubmissionId: string,\n\tmediaType: 'image' | 'video',\n\tdwellMs: number\n) {\n\tevent('arena_view', { submission_id: submissionId, media_type: mediaType, dwell_ms: dwellMs });\n}\n\n// ──────────────────────────────────────────\n// Beat me events (RAT-76)\n//\n// Funnel: listing_view → challenge_create (A) → challenge_view (B cold) →\n// challenge_accept (B) → challenge_vote (any voter). Share + copy + reddit\n// click events feed the virality side. challenge_view fires for every state\n// (pending/live/closed/expired) with viewer role so we can split the funnel.\n// ──────────────────────────────────────────\n\nexport type BeatViewer = 'owner' | 'accepter' | 'voter' | 'visitor';\nexport type BeatStatusForView = 'pending' | 'live' | 'closed' | 'expired';\nexport type BeatSharePosition = 'waiting' | 'live';\nexport type BeatVoteRejectReason = 'already_voted' | 'self_vote' | 'not_live';\n\n// RAT-207: trackBeatChallengeCreate moved server-side to api/beat/+server.ts.\n\n/** B successfully accepted a challenge (FCFS-winner). */\nexport function trackBeatChallengeAccept(code: string, mediaType: 'image' | 'video') {\n\tevent('beat_challenge_accept', { code, media_type: mediaType });\n}\n\n/** B tried to accept but lost the FCFS race (server returned 409). Critical\n *  for measuring conversion of race-losers into \"start their own\" challenges. */\nexport function trackBeatChallengeAcceptLost(code: string) {\n\tevent('beat_challenge_accept_lost', { code });\n}\n\n// RAT-207: trackBeatVote moved server-side to api/beat/[code]/vote/+server.ts.\n\nexport function trackBeatVoteRejected(code: string, reason: BeatVoteRejectReason) {\n\tevent('beat_vote_rejected', { code, reason });\n}\n\nexport function trackBeatLinkCopied(code: string, position: BeatSharePosition) {\n\tevent('beat_link_copied', { code, position });\n}\n\nexport function trackBeatRedditClick(code: string, subreddit: string, position: BeatSharePosition) {\n\tevent('beat_reddit_click', { code, subreddit, position });\n}\n\n/** Page view on /b/<code>. Fires once per mount with the current state +\n *  viewer role so we can decompose the funnel into owner/accepter/voter\n *  cohorts and pending→live→closed transitions. */\nexport function trackBeatChallengeView(\n\tcode: string,\n\tstatus: BeatStatusForView,\n\tviewer: BeatViewer\n) {\n\tevent('beat_challenge_view', { code, status, viewer });\n}\n\nexport function trackBeatListingView(counts: {\n\tlive: number;\n\tpending: number;\n\tclosed: number;\n\thasOwn: boolean;\n}) {\n\tevent('beat_listing_view', {\n\t\tlive_count: counts.live,\n\t\tpending_count: counts.pending,\n\t\tclosed_count: counts.closed,\n\t\thas_own: counts.hasOwn\n\t});\n}\n\n/** RAT-230: fires once per mount on /b when the user has scrolled past\n *  50% of the document. Engagement signal for the post-update feedback\n *  PostHog Survey. */\nexport function trackBeatListingScrolled50() {\n\tevent('beat_listing_scrolled_50');\n}\n\n// ──────────────────────────────────────────\n// Apply events\n// ──────────────────────────────────────────\n\nexport function trackRaterApplicationSubmit(style: string) {\n\tevent('rater_application_submit', { style });\n}\n\n// ──────────────────────────────────────────\n// Human-rating flow (RAT-182)\n// ──────────────────────────────────────────\n\n// Tower of the funnel — visitor lands on a rater's public page.\nexport function trackRaterProfileViewed(opts: {\n\traterUserId: string;\n\tnickname: string;\n\traterIsLive: boolean;\n\tqueueDepth: number;\n\tpackageCount: number;\n\tminPriceCents: number;\n\thasFreePackage: boolean;\n\tutmSource?: string | null;\n}) {\n\tevent('rater_profile_viewed', {\n\t\trater_user_id: opts.raterUserId,\n\t\tnickname: opts.nickname,\n\t\trater_is_live: opts.raterIsLive,\n\t\tqueue_depth: opts.queueDepth,\n\t\tpackage_count: opts.packageCount,\n\t\tmin_price_cents: opts.minPriceCents,\n\t\thas_free_package: opts.hasFreePackage,\n\t\tutm_source: opts.utmSource ?? null\n\t});\n}\n\n// RAT-207: trackRaterRequestSubmitted moved server-side. Fires from\n// r/[nickname]/request/+page.server.ts (free/dev) and api/stripe/webhook/+server.ts (paid).\n//\n// RAT-207: trackRaterSessionStarted moved server-side to profile/+page.server.ts\n// (startRateRequest action). Fires once each for role=rater and role=requester\n// when the rater accepts a queued request and the session is created.\n\n// Tab close / nav-away while the session is still active. Fires from\n// pagehide so we catch iOS Safari swipes too. secondsIn pinpoints which\n// stretch of the session bleeds users.\nexport function trackRaterSessionAbandoned(opts: {\n\tsessionId: string;\n\trole: 'rater' | 'requester';\n\tsecondsIn: number;\n}) {\n\tevent('rater_session_abandoned', {\n\t\tsession_id: opts.sessionId,\n\t\trole: opts.role,\n\t\tseconds_in: opts.secondsIn\n\t});\n}\n\n// Follow-button transitions on the rater's public page or post-session UI.\n// Only fires on the not-following → following edge (not on unfollow).\nexport function trackRaterFollowed(opts: {\n\traterUserId: string;\n\tsource: 'rater_profile' | 'post_session' | 'other';\n}) {\n\tevent('rater_followed', {\n\t\trater_user_id: opts.raterUserId,\n\t\tsource: opts.source\n\t});\n}\n\n// RAT-207: trackRaterLiveToggled moved server-side to profile/+page.server.ts\n// (toggleRaterActive action).\n//\n// RAT-207: trackRaterRequestAccepted moved server-side to profile/+page.server.ts\n// (startRateRequest action), fires alongside rater_session_started.\n\n// Rater dismissed a queued request without starting a session (skip).\nexport function trackRaterRequestSkipped(opts: { requestId: string }) {\n\tevent('rater_request_skipped', {\n\t\trequest_id: opts.requestId\n\t});\n}\n\n// Rater marked a request done outside the normal session-end path\n// (administrative complete on /profile queue).\nexport function trackRaterRequestCompleted(opts: { requestId: string }) {\n\tevent('rater_request_completed', {\n\t\trequest_id: opts.requestId\n\t});\n}\n\n// ──────────────────────────────────────────\n// Live cam — go-live / greenroom setup\n//\n// The livecam flow previously had zero client instrumentation, so setup\n// friction on /livecam/start was only observable in session replay. These\n// cover the greenroom → go-live funnel and the tip-menu edits along the way.\n// ──────────────────────────────────────────\n\n/** Greenroom setup page (/livecam/start) mounted. */\nexport function trackLivecamGreenroomView() {\n\tevent('livecam_greenroom_view');\n}\n\nexport function trackLivecamPresetApplied(preset: string, tipCount: number) {\n\tevent('livecam_preset_applied', { preset, tip_count: tipCount });\n}\n\nexport function trackLivecamTipAdded(tipCount: number) {\n\tevent('livecam_tip_added', { tip_count: tipCount });\n}\n\nexport function trackLivecamTipRemoved(tipCount: number) {\n\tevent('livecam_tip_removed', { tip_count: tipCount });\n}\n\n/** \"go live on cam\" clicked — fires on the request attempt (before the result). */\nexport function trackLivecamGoLive(opts: {\n\ttipCount: number;\n\thasGoal: boolean;\n\tpreset: string | null;\n}) {\n\tevent('livecam_go_live', {\n\t\ttip_count: opts.tipCount,\n\t\thas_goal: opts.hasGoal,\n\t\tpreset: opts.preset\n\t});\n}\n\n/** Room created — creator is live. */\nexport function trackLivecamLiveStarted(sessionId: string) {\n\tevent('livecam_live_started', { session_id: sessionId });\n}\n\nexport function trackLivecamGoLiveFailed(reason: string) {\n\tevent('livecam_go_live_failed', { reason });\n}\n\n/** Viewer's WHEP playback stalled or errored into a black feed. Before this the\n *  viewer side had zero error instrumentation, so the black-feed rate was\n *  invisible in analytics — we couldn't tell how often a stall left someone\n *  stuck. `reason` distinguishes the failure mode so the fixes can be measured:\n *  `pc_failed` / `pc_disconnected` (connection state), `connect_timeout` (ICE/NAT\n *  stuck in `connecting`, never reaches `failed`), `no_video_track` (only audio\n *  arrived → black video), `no_media` (nothing arrived), `whep_http_<status>`\n *  (ingest POST rejected), `whep_exception` (offer/answer threw).\n *\n *  `sessionStatus` is the SESSION's lifecycle state (live / pending / ended).\n *  Deliberately not named `status`: `reason` already embeds an HTTP status\n *  (`whep_http_404`), and one event carrying two different meanings of \"status\"\n *  is how a breakdown gets misread later. */\nexport function trackLivecamViewerStall(opts: {\n\tsessionId: string;\n\treason: string;\n\tsessionStatus: string;\n}) {\n\tevent('livecam_viewer_stall', {\n\t\tsession_id: opts.sessionId,\n\t\treason: opts.reason,\n\t\tsession_status: opts.sessionStatus\n\t});\n}\n\n// Peer rate (DM chat) + floor queue\n//\n// The rate-request / floor-queue flow monetizes the product (tokens + escrow)\n// but was fully uninstrumented — the drop-off between opening a rate request\n// and actually submitting it was invisible in PostHog. These events make the\n// funnel measurable: open → submit vs open → close, and get-in-line → joined.\n// ──────────────────────────────────────────\n\n/** Recipient opened the rating modal for a pending peer-rate request. */\nexport function trackPeerRateOpened(peerRateId: string | null) {\n\tevent('peer_rate_opened', { peer_rate_id: peerRateId });\n}\n\n/** Recipient submitted a peer rating (modal → rate_card flip). */\nexport function trackPeerRateSubmitted(peerRateId: string | null, overall: number) {\n\tevent('peer_rate_submitted', { peer_rate_id: peerRateId, overall });\n}\n\n/** Recipient closed the rating modal without submitting. `dirty` = a slider had\n *  moved or a verdict was typed (a real abandon, not an idle open-and-close). */\nexport function trackPeerRateClosed(peerRateId: string | null, dirty: boolean) {\n\tevent('peer_rate_closed', { peer_rate_id: peerRateId, dirty });\n}\n\n// Requester side of the DM peer-rate flow (the monetized rating loop). This is\n// the funnel that was fully dark: initiated (rate flow started) → sent (photo\n// picked, upload fired) → success | failed. The initiated→sent gap captures\n// drop-off at the OS file picker / package picker; failed carries a `reason`\n// so a stalled-network abort is distinguishable from a server rejection.\n\n/** Requester started a peer-rate request (tapped the rate pill / attach item). */\nexport function trackPeerRateRequestInitiated(opts: { raterUserId: string; needsPick: boolean }) {\n\tevent('peer_rate_request_initiated', {\n\t\trater_user_id: opts.raterUserId,\n\t\tneeds_pick: opts.needsPick\n\t});\n}\n\n/** Requester picked a photo and the upload POST to /api/peer-rate fired. */\nexport function trackPeerRateRequestSent(opts: { raterUserId: string; priceTokens: number }) {\n\tevent('peer_rate_request_sent', {\n\t\trater_user_id: opts.raterUserId,\n\t\tprice_tokens: opts.priceTokens\n\t});\n}\n\n/** The peer-rate request persisted (server 2xx) — bubble confirmed sent. */\nexport function trackPeerRateRequestSuccess(opts: { peerRateId: string; priceTokens: number }) {\n\tevent('peer_rate_request_success', {\n\t\tpeer_rate_id: opts.peerRateId,\n\t\tprice_tokens: opts.priceTokens\n\t});\n}\n\n/** The peer-rate request failed. `reason`: 'timeout' | 'network' | 'insufficient_tokens' | 'server'. */\nexport function trackPeerRateRequestFailed(opts: { reason: string; priceTokens: number }) {\n\tevent('peer_rate_request_failed', {\n\t\treason: opts.reason,\n\t\tprice_tokens: opts.priceTokens\n\t});\n}\n\n// ──────────────────────────────────────────\n// Chat dead ends (RAT: silent drop-offs on the chat surface)\n//\n// Three chat paths used to fail invisibly — no toast, no capture — so we could\n// only guess at how often they hurt: the blocked/banned composer that renders\n// fully inert, a muted-autoplay rejection that leaves a video sitting static,\n// and a client-side poster capture that quietly falls back to no thumbnail.\n// These make each measurable.\n\n/** The blocked/banned composer rendered fully inert (no reply possible). */\nexport function trackChatComposerLocked(opts: {\n\tsurface: 'dm' | 'group';\n\treason: 'banned' | 'blocked_by_me' | 'blocked_by_them';\n}) {\n\tevent('chat_composer_locked', { surface: opts.surface, reason: opts.reason });\n}\n\n/** Muted autoplay was rejected in the media lightbox → the clip sits static.\n *  `reason` carries the DOMException name (NotAllowedError, etc.). */\nexport function trackVideoAutoplayBlocked(surface: 'chat', reason: string) {\n\tevent('video_autoplay_blocked', { surface, reason });\n}\n\n/** Client-side poster-frame capture failed → the video sends without a thumbnail. */\nexport function trackVideoPosterFallback(surface: 'dm' | 'group' | 'rate') {\n\tevent('video_poster_fallback', { surface });\n}\n\n/** Viewer tapped \"get in line\" on a floor (opened the package picker). */\nexport function trackFloorGetInLineTapped(sessionId: string) {\n\tevent('floor_get_in_line_tapped', { session_id: sessionId });\n}\n\n/** Viewer's floor-queue join attempt resolved (file picked → server responded). */\nexport function trackFloorQueueJoined(sessionId: string, outcome: 'success' | 'failure') {\n\tevent('floor_queue_joined', { session_id: sessionId, outcome });\n}\n\n// ──────────────────────────────────────────\n// Token wall / buy-tokens funnel\n//\n// A paid live action (floor queue tier, livecam boost) blocked because the\n// viewer was short on tokens used to fire NO event — the floor just flashed a\n// toast and dead-ended, so the abandonment moment was invisible. These make the\n// wall → buy-tokens-modal → purchase funnel measurable. `surface` splits the\n// floor queue picker from the livecam boost.\n// ──────────────────────────────────────────\n\nexport function trackTokenWallHit(opts: {\n\tsurface: 'floor_queue' | 'floor_tip' | 'livecam_boost';\n\tsessionId: string | null;\n\ttokensRequired?: number | null;\n\ttokensHeld?: number | null;\n}) {\n\tevent('token_wall_hit', {\n\t\tsurface: opts.surface,\n\t\tsession_id: opts.sessionId,\n\t\ttokens_required: opts.tokensRequired ?? null,\n\t\ttokens_held: opts.tokensHeld ?? null\n\t});\n}\n\n/** The buy-tokens modal opened. `source` is the surface that raised it (the same\n *  value forwarded to Stripe checkout), so the open → purchase funnel can split\n *  by where the token wall was hit. */\nexport function trackBuyTokensModalOpen(source: string) {\n\tevent('buy_tokens_modal_open', { source });\n}\n\n// ──────────────────────────────────────────\n// Auth events\n// ──────────────────────────────────────────\n\nexport function trackSignInStart() {\n\tevent('sign_in_start');\n}\n\nexport function trackSignInComplete() {\n\tevent('sign_in_complete');\n}\n\n// OAuth click path (\"continue with google\").\n//\n// The button that fronts every email gate (/auth/sign-in, /d, /a/submit via\n// OAuthButtonRow, plus the mobile AuthSheet) had ZERO instrumentation: a tap\n// that hung on \"redirecting…\" or silently reverted left no trace outside\n// session replay, so this class of bug stayed invisible until a recording\n// surfaced it. `trackOAuthStart` fires on the tap; `trackOAuthFailed` fires\n// once per client-side failure branch BEFORE the provider redirect, with\n// `reason` splitting the failure mode:\n//   prepare_refused   — beforeRedirect hook returned false (upload/save failed)\n//   prepare_error     — beforeRedirect hook threw\n//   provider_error    — signInWithOAuth returned an error\n//   redirect_error    — the warm-import / signInWithOAuth call threw\n//   watchdog_timeout  — no navigation happened within the watchdog window\n// A failure here means the user never reached Google — the redirect never left\n// the page — so it is NOT the dropped-handshake case on the return leg (that\n// one bounces through /auth/error). `surface` is the gate the tap came from\n// (the `next` deeplink, e.g. /d, /a/submit, or the auth-sheet flow label).\nexport function trackOAuthStart(provider: string, surface: string) {\n\tevent('oauth_start', { provider, surface });\n}\n\nexport function trackOAuthFailed(provider: string, reason: string, surface: string) {\n\tevent('oauth_failed', { provider, reason, surface });\n}\n\n// ──────────────────────────────────────────\n// Social events (RAT-7 epic — follow graph, profile views)\n// ──────────────────────────────────────────\n\nexport function trackProfileView(opts: {\n\tisOwnProfile: boolean;\n\tviewerFollowsOwner: boolean;\n\tutmSource?: string | null;\n}) {\n\tevent('profile_view', {\n\t\tis_own_profile: opts.isOwnProfile,\n\t\tviewer_follows_owner: opts.viewerFollowsOwner,\n\t\tutm_source: opts.utmSource ?? null\n\t});\n}\n\nexport function trackFollowListView(\n\ttype: 'followers' | 'following',\n\topts: { listOwner: string; isOwnList: boolean }\n) {\n\tevent(type === 'followers' ? 'followers_list_viewed' : 'following_list_viewed', {\n\t\tlist_owner: opts.listOwner,\n\t\tis_own_list: opts.isOwnList\n\t});\n}\n\nexport function trackProfileShareClick(opts: {\n\tisOwnProfile: boolean;\n\tmethod: 'web_share' | 'clipboard';\n}) {\n\tevent('profile_share_clicked', {\n\t\tis_own_profile: opts.isOwnProfile,\n\t\tmethod: opts.method\n\t});\n}\n\nexport function trackPostDuelFollowCtaShown(opts: { duelCode: string; opponentUserId: string }) {\n\tevent('post_duel_follow_cta_shown', {\n\t\tduel_code: opts.duelCode,\n\t\topponent_user_id: opts.opponentUserId\n\t});\n}\n\n// ──────────────────────────────────────────\n// Notifications (RAT-21)\n// ──────────────────────────────────────────\n\nexport function trackNotificationDropdownOpened(opts: { unreadCount: number }) {\n\tevent('notification_dropdown_opened', { unread_count: opts.unreadCount });\n}\n\nexport function trackNotificationClicked(opts: { type: string; notificationId: string }) {\n\tevent('notification_clicked', {\n\t\ttype: opts.type,\n\t\tnotification_id: opts.notificationId\n\t});\n}\n\n// ──────────────────────────────────────────\n// Realtime connection state (RAT-370)\n// ──────────────────────────────────────────\n\n/** Fires once per real connection-state transition in the RealtimeClient.\n *  Before this we were flying blind: the only trace of a \"reconnecting…\" /\n *  \"offline\" pill was in session recordings (no `$exception`, no capture), so\n *  we couldn't measure how often it happened or whether a fix moved the needle.\n *\n *  `state` is the state we moved INTO, `from` the prior one. `degraded_ms` is\n *  set only on recovery (→ connected) and measures how long the degraded window\n *  lasted — the number that tells us whether channels-warm-across-nav helped. */\nexport function trackRealtimeConnectionState(props: {\n\tstate: 'connected' | 'reconnecting' | 'offline';\n\tfrom: 'connected' | 'reconnecting' | 'offline';\n\tchannelCount: number;\n\thealthyCount: number;\n\t/** Kinds (the part before `:`) of the channels a component listens on, and\n\t *  of those that were down when the pill went up — e.g. \"chat,user\". */\n\tchannelKinds?: string | null;\n\tunhealthyKinds?: string | null;\n\t/** Visible time only: the degraded window minus time spent in the background. */\n\tdegradedMs?: number | null;\n\thiddenMs?: number | null;\n\tpath?: string | null;\n}) {\n\tevent('realtime_connection_state', {\n\t\tstate: props.state,\n\t\tfrom: props.from,\n\t\tchannel_count: props.channelCount,\n\t\thealthy_count: props.healthyCount,\n\t\tchannel_kinds: props.channelKinds ?? null,\n\t\tunhealthy_kinds: props.unhealthyKinds ?? null,\n\t\tdegraded_ms: props.degradedMs ?? null,\n\t\thidden_ms: props.hiddenMs ?? null,\n\t\tpath: props.path ?? null\n\t});\n}\n\n/** RAT-461: the catch-up fetch that surfaces a roleplay move (the player's own\n *  action + the GM's reply) failed every retry. Client-side by nature — a drop\n *  here means the user may be staring at a chat that never showed their message,\n *  the exact silent failure that only turned up in session replays. `attempts` is\n *  how many tries were made before giving up. */\nexport function trackRoleplayMessagePullFailed(props: {\n\troomId: string;\n\tkind: string;\n\tattempts: number;\n}) {\n\tevent('roleplay_message_pull_failed', {\n\t\troom_id: props.roomId,\n\t\tkind: props.kind,\n\t\tattempts: props.attempts\n\t});\n}\n\n// ──────────────────────────────────────────\n// Roleplay character chat (RAT-463)\n// ──────────────────────────────────────────\n// The 1:1 chat shipped with no funnel at all: a sent message, a provider content\n// block, a quota wall and a failed generation were every one of them invisible,\n// so the asterisk-wall failure mode — a \"successful\" reply the user can't read —\n// only ever turned up in session replays. These make the failure edges countable.\n\n// `chatId` is nullable throughout: since #1079 a logged-out visitor gets a\n// read-only preview with no chat row yet. Those paths bail at the auth gate\n// before they'd fire, so a null here means the event came from somewhere it\n// shouldn't have — worth seeing in the data rather than hiding behind a `!`.\n\nexport function trackRoleplayMessageSent(chatId: string | null) {\n\tevent('roleplay_message_sent', { chat_id: chatId });\n}\n\n/** A turn came back with no reply. `stage` separates the first send from a retry\n *  tap; `reason` splits a server non-answer (empty / refused-and-regenerated /\n *  502) from a dropped connection. */\nexport function trackRoleplayGenerationFailed(props: {\n\tchatId: string | null;\n\tstage: 'send' | 'retry';\n\treason: string;\n}) {\n\tevent('roleplay_generation_failed', {\n\t\tchat_id: props.chatId,\n\t\tstage: props.stage,\n\t\treason: props.reason\n\t});\n}\n\n/** The AI provider refused the scene on content policy — a permanent,\n *  thread-scoped block (not a retryable error). Fires once when the blocked\n *  state is first shown, so refusal frequency is measurable and the server-side\n *  quota refund can be verified against it. `surface` splits which action met\n *  it (send / retry / photo); `chat_id` is null for a logged-out preview. */\nexport function trackRoleplayBlocked(chatId: string | null, surface: string) {\n\tevent('roleplay_blocked', { chat_id: chatId, surface });\n}\n\nexport function trackRoleplayQuotaHit(chatId: string | null) {\n\tevent('roleplay_quota_hit', { chat_id: chatId });\n}\n\n// ──────────────────────────────────────────\n// Targeted duel challenges (RAT-9)\n// ──────────────────────────────────────────\n\nexport function trackChallengeNotificationShown(opts: {\n\tnotificationId: string;\n\tduelCode: string | null;\n\tchallengerUserId: string | null;\n}) {\n\tevent('challenge_notification_shown', {\n\t\tnotification_id: opts.notificationId,\n\t\tduel_code: opts.duelCode,\n\t\tchallenger_user_id: opts.challengerUserId\n\t});\n}\n\nexport function trackChallengeNotificationClicked(opts: {\n\tnotificationId: string;\n\tduelCode: string | null;\n\tchallengerUserId: string | null;\n}) {\n\tevent('challenge_notification_clicked', {\n\t\tnotification_id: opts.notificationId,\n\t\tduel_code: opts.duelCode,\n\t\tchallenger_user_id: opts.challengerUserId\n\t});\n}\n\n// ──────────────────────────────────────────\n// Roadmap board\n// ──────────────────────────────────────────\n// The board shipped with zero instrumentation, so none of its funnel was\n// measurable. `source` is the important property everywhere: it separates\n// the in-feed rail from people who navigated to /feedback on their own.\n\nexport function trackRoadmapRailViewed(itemCount: number) {\n\tevent('roadmap_rail_viewed', { item_count: itemCount });\n}\n\nexport function trackRoadmapVote(feedbackId: string, voted: boolean, source: string) {\n\tevent('feedback_voted', { feedback_id: feedbackId, voted, source });\n}\n\nexport function trackRoadmapRailBrowseClick() {\n\tevent('roadmap_rail_browse_click', {});\n}\n\nexport function trackRoadmapRailCreateClick() {\n\tevent('roadmap_rail_create_click', {});\n}\n\nexport function trackFeedbackSubmitted(source: string, type: string) {\n\tevent('feedback_submitted', { source, type });\n}\n\n// ──────────────────────────────────────────\n// Chat send failures\n//\n// `chat_message_sent` only fires server-side on SUCCESS, so the failure path\n// was invisible in analytics — a permanent block rendered as a retryable glitch\n// left no trace outside session replay. This fires once per definitive client\n// failure. `surface` splits DM vs group; `reason` is the server error code (or\n// 'network' for a dropped request); `permanent` flags a 4xx that can never\n// succeed on retry (block / non-member / admins-only / banned) vs a transient one.\n// ──────────────────────────────────────────\n\nexport function trackChatSendFailed(props: {\n\tsurface: 'dm' | 'group';\n\treason: string;\n\tpermanent: boolean;\n}) {\n\tevent('chat_send_failed', {\n\t\tsurface: props.surface,\n\t\treason: props.reason,\n\t\tpermanent: props.permanent\n\t});\n}\n\n/** RAT-522: an opener chip was tapped — sent on the spot in the onetap arm,\n *  put in the composer in control (`one_tap` false). `surface` is the empty\n *  DM or the arena match overlay; `context` which openers were on offer. */\nexport function trackChatOpenerTapped(p: {\n\tcontext: 'arena_match' | 'rater' | 'dm' | 'legacy';\n\tsurface: 'dm' | 'match_overlay';\n\toneTap: boolean;\n}) {\n\tevent('chat_opener_tapped', { context: p.context, surface: p.surface, one_tap: p.oneTap });\n}\n\n// ──────────────────────────────────────────\n// Roleplay free-tier wall\n//\n// The roleplay flow shipped with zero client instrumentation — no capture under\n// /roleplay at all — so the free-tier wall and provider content-blocks were\n// invisible outside session replay. We couldn't count how many people meet the\n// wall, how many hits were bought with a refused turn, or how many convert.\n// `surface` splits which action met it (text / photo / retry).\n// ──────────────────────────────────────────\n\n/** Free daily message allowance spent — the in-scene wall was shown. */\nexport function trackRoleplayWallHit(surface: string) {\n\tevent('roleplay_wall_hit', { surface });\n}\n\n/* trackRoleplayBlocked lives with the other roleplay events above — it carries\n * both `chat_id` (#1089) and `surface` (#1063). */\n\n// ──────────────────────────────────────────\n// Next-step offers (RAT-505)\n//\n// A product offered at a \"what now?\" moment: the /rate \"not ready to upload?\"\n// sheet (placement `rate_not_now`, RAT-507) and the result page's next step\n// (`result_next`, RAT-508). `products` is what the surface offered, in order,\n// so a click's `position` can be read against it.\n// ──────────────────────────────────────────\n\nexport function trackOfferShown(\n\tplacement: string,\n\tproducts: string[],\n\textra: Record<string, string | number> = {}\n) {\n\tevent('offer_shown', { placement, products, ...extra });\n}\n\nexport function trackOfferClicked(\n\tplacement: string,\n\tproduct: string,\n\tposition: number,\n\textra: Record<string, string | number> = {}\n) {\n\tevent('offer_clicked', { placement, product, position, ...extra });\n}\n\n/** The surface was closed without a pick: `how` is close / backdrop / escape,\n *  or `upload` for the sheet's \"upload a photo after all\". */\nexport function trackOfferDismissed(placement: string, how: string) {\n\tevent('offer_dismissed', { placement, how });\n}\n\n/** RAT-515: a finished studio clip started playing — once per clip per tab.\n *  `autoplayed` is the stage starting it on its own as the render lands, against\n *  the viewer pressing play on one they came back to. */\nexport function trackStudioClipViewed(props: { autoplayed: boolean }) {\n\tevent('studio_clip_viewed', props);\n}\n\n/** Guided sessions (Epic Q): the page's half of the voice_session_* events —\n *  the server sends created/first_ready/told/extended/exported/ended/rated.\n *  `name` is the step: mode, setup_opened, play_started, chip, tell_opened,\n *  download, extend_pressed. */\nexport function trackVoiceSession(name: string, props: Record<string, unknown> = {}) {\n\tevent(`voice_session_${name}`, props);\n}\n\n/** The first-visit chooser between a clip and a session: which one was picked,\n *  or `skip` for \"Just look around\". */\nexport function trackOnboardingChoice(choice: 'clip' | 'session' | 'skip') {\n\tevent('onboarding_choice', { choice });\n}\n\n/** RAT-632: a prompt from PornPilot's library (/prompts) copied, or opened in\n *  the studio — the step between a search visit and a first shot. The prompt's\n *  id and its group, so the library can be pruned to what people take. */\nexport function trackStudioPrompt(action: 'copied' | 'opened', props: { prompt: string; group: string }) {\n\tevent(`studio_prompt_${action}`, props);\n}\n\n// ──────────────────────────────────────────\n// Inbox \"online now\" row (RAT-537)\n//\n// A new account's inbox opens with people who are on right now and answer\n// first messages. `shown` once per inbox visit with how many faces; `tapped`\n// with the face's position and its usual reply time. Whether the chat got an\n// answer is read from the DB, not from here.\n// ──────────────────────────────────────────\n\nexport function trackOnlineNowShown(count: number) {\n\tevent('inbox_online_now_shown', { count });\n}\n\nexport function trackOnlineNowTapped(position: number, replySeconds: number, isRater: boolean) {\n\tevent('inbox_online_now_tapped', {\n\t\tposition,\n\t\treply_seconds: Math.round(replySeconds),\n\t\tis_rater: isRater\n\t});\n}\n"],"version":3}