mcp-scraper 0.88.2 → 0.89.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +41 -2
  2. package/README.md +6 -3
  3. package/dist/analytics-repository-J25XR5E7.js +1 -0
  4. package/dist/bin/api-server.js +2 -41
  5. package/dist/bin/mcp-scraper-cli.js +39 -756
  6. package/dist/bin/mcp-scraper-core.js +1 -60
  7. package/dist/bin/mcp-scraper-install.js +2 -25
  8. package/dist/bin/mcp-stdio-server.js +1 -19
  9. package/dist/bin/paa-harvest.js +1 -41
  10. package/dist/chunk-3GP5CYZX.js +1 -0
  11. package/dist/chunk-4FROKQJN.js +1 -0
  12. package/dist/chunk-57AHYL2O.js +20 -0
  13. package/dist/chunk-5H3MCTTP.js +1 -0
  14. package/dist/chunk-63T4W2LX.js +1271 -0
  15. package/dist/chunk-645LQBE7.js +2595 -0
  16. package/dist/chunk-6QO4F2KC.js +21710 -0
  17. package/dist/chunk-CCYWSJNG.js +16 -0
  18. package/dist/chunk-D6IQJP64.js +84 -0
  19. package/dist/chunk-DGBLGFNJ.js +182 -0
  20. package/dist/chunk-FFPD2DOF.js +59 -0
  21. package/dist/chunk-FXEWHK54.js +1 -0
  22. package/dist/chunk-HE45FFBU.js +1 -0
  23. package/dist/chunk-HUV2WTRW.js +1 -0
  24. package/dist/chunk-IJ2AO7AO.js +102 -0
  25. package/dist/chunk-KJQXUZ4Y.js +4 -0
  26. package/dist/chunk-MEM4D2QN.js +1280 -0
  27. package/dist/chunk-MZN4U5BL.js +1 -0
  28. package/dist/chunk-PRWZNHQB.js +1 -0
  29. package/dist/chunk-QPWPR5XG.js +10 -0
  30. package/dist/chunk-RYNHNEF3.js +73 -0
  31. package/dist/chunk-TMB56NCA.js +1 -0
  32. package/dist/chunk-W2BVJ7S2.js +13 -0
  33. package/dist/chunk-WO3N5FH2.js +5 -0
  34. package/dist/chunk-XLWNEVUZ.js +27 -0
  35. package/dist/chunk-YQZGZBB4.js +1 -0
  36. package/dist/chunk-YUPLOOTB.js +4 -0
  37. package/dist/chunk-ZHORX44A.js +100 -0
  38. package/dist/db-JJWJLAVQ.js +1 -0
  39. package/dist/extract-bundle-TTSEGWYF.js +26 -0
  40. package/dist/gmail-service-34PIJRLK.js +1 -0
  41. package/dist/index.cjs +21838 -6034
  42. package/dist/index.js +18 -315
  43. package/dist/lead-list-enrichment-repository-VJBBMBCG.js +1 -0
  44. package/dist/location-data-repository-W3LPT6G3.js +1 -0
  45. package/dist/server-UEY5YQL6.js +7329 -0
  46. package/dist/site-extract-repository-7W5ZUNPR.js +1 -0
  47. package/dist/worker-ZOTOKG56.js +1 -0
  48. package/package.json +11 -4
  49. package/dist/analytics-repository-GGJJCVVP.js +0 -194
  50. package/dist/chunk-4QMUF6XM.js +0 -1013
  51. package/dist/chunk-6HAV7LCE.js +0 -265
  52. package/dist/chunk-ABF2CGOZ.js +0 -113
  53. package/dist/chunk-C5Z4OFKW.js +0 -404
  54. package/dist/chunk-DNM65UCK.js +0 -299
  55. package/dist/chunk-EQGTEHLZ.js +0 -592
  56. package/dist/chunk-F5GQJWZU.js +0 -732
  57. package/dist/chunk-GGZEC22A.js +0 -215
  58. package/dist/chunk-GXBZXWXB.js +0 -184
  59. package/dist/chunk-IHXAXYIS.js +0 -843
  60. package/dist/chunk-K3Z5AQYE.js +0 -683
  61. package/dist/chunk-K45K75OF.js +0 -6
  62. package/dist/chunk-LFW2FRPJ.js +0 -224
  63. package/dist/chunk-MZDNZQWT.js +0 -2078
  64. package/dist/chunk-NVUKO5NN.js +0 -256
  65. package/dist/chunk-OM7HVEJ3.js +0 -26
  66. package/dist/chunk-OPQIGAFB.js +0 -286
  67. package/dist/chunk-OZJMVCDK.js +0 -16
  68. package/dist/chunk-P7FWOMU7.js +0 -505
  69. package/dist/chunk-PGJQDMC2.js +0 -383
  70. package/dist/chunk-PKZS6SHW.js +0 -33139
  71. package/dist/chunk-RJ7JVYKU.js +0 -68
  72. package/dist/chunk-S24LFPL7.js +0 -5262
  73. package/dist/chunk-T3MZISOF.js +0 -240
  74. package/dist/chunk-UZPTGUDV.js +0 -1915
  75. package/dist/chunk-X623GTBV.js +0 -8290
  76. package/dist/chunk-YXNDOQXN.js +0 -4018
  77. package/dist/db-Z34LPZNR.js +0 -284
  78. package/dist/extract-bundle-565SBZCR.js +0 -1003
  79. package/dist/gmail-service-E6ALS7JG.js +0 -25
  80. package/dist/lead-list-enrichment-repository-36RPVV6N.js +0 -67
  81. package/dist/location-data-repository-WPRG62GE.js +0 -34
  82. package/dist/server-SQZ3A7SY.js +0 -86606
  83. package/dist/site-extract-repository-VYFZASPU.js +0 -69
  84. package/dist/worker-LDCAULWL.js +0 -146
@@ -0,0 +1,2595 @@
1
+ import{a as Q,d as Se}from"./chunk-D6IQJP64.js";import{createHash as pe,createHmac as Yt,randomBytes as P,randomUUID as y}from"crypto";import{Pool as Wt}from"pg";var ct=[[/perplexity/i,"llm","Perplexity"],[/chatgpt|chat\.openai|openai/i,"llm","ChatGPT"],[/claude|anthropic/i,"llm","Claude"],[/\bgrok\b|xai/i,"llm","Grok"],[/ai[ _-]?overview|google[ _-]?ai|\bsge\b|gemini/i,"llm","Google AI Overview"],[/instagram/i,"social","Instagram"],[/facebook|\bfb\b|meta/i,"social","Facebook"],[/linkedin/i,"social","LinkedIn"],[/tiktok/i,"social","TikTok"],[/youtube|youtu\.be/i,"social","YouTube"],[/twitter|x\.com/i,"social","X"],[/\bg2\b|g2\.com/i,"review","G2"],[/trustpilot/i,"review","Trustpilot"],[/\bbbb\b|better business bureau/i,"review","BBB"],[/capterra/i,"review","Capterra"],[/yelp/i,"review","Yelp"],[/google|bing|duckduckgo|yahoo/i,"search","Search"]];function q(e,t=240){return typeof e=="string"&&e.trim()?e.trim().slice(0,t):null}function Ce(e){try{return e?new URL(e).hostname.replace(/^www\./,""):""}catch{return""}}function Re(e){let t=e.properties??{},i=q(e.source,180)||Ce(e.referrer)||"(direct)",n=q(e.medium,180)||(i==="(direct)"?"(none)":"referral"),s=e.clickIds??{},a=`${i} ${n} ${Ce(e.referrer)}`,r=i==="(direct)"?"direct":/email|newsletter/i.test(n)?"email":"referral",o=i==="(direct)"?"Direct":i;for(let[d,_,u]of ct)if(d.test(a)){r=_,o=u;break}return s.fbclid?(r="social",o=/instagram/i.test(a)?"Instagram":"Facebook"):s.ttclid?(r="social",o="TikTok"):s.rdt_cid?(r="social",o="Reddit"):s.gclid||s.gbraid||s.wbraid?(r="search",o="Google"):s.msclkid&&(r="search",o="Microsoft Ads"),{channelFamily:r,platform:o,source:i,medium:n,campaign:q(e.campaign),campaignId:q(t.campaign_id??t.utm_id),adSetId:q(t.adset_id??t.ad_set_id??t.adgroup_id??t.ad_group_id),adId:q(t.ad_id),creativeId:q(t.creative_id),placement:q(t.placement),term:q(t.utm_term),content:q(t.utm_content)}}import{createHash as yt,randomUUID as ee}from"crypto";import{createCipheriv as _t,createDecipheriv as ut,createHmac as fe,randomBytes as Et,scryptSync as lt,timingSafeEqual as Nt}from"crypto";var Z="xray1";function mt(){return process.env.ANALYTICS_RESTRICTED_DATA_SECRET?.trim()||Q()}function _e(e){return lt(mt(),`xray-restricted:${e}:v1`,32)}function De(e,t){let i=t.trim().slice(0,80);if(!i)throw new Error("analytics encryption purpose required");let n=Et(12),s=_t("aes-256-gcm",_e(i),n);s.setAAD(Buffer.from(`${Z}:${i}`,"utf8"));let a=Buffer.concat([s.update(e,"utf8"),s.final()]),r=s.getAuthTag();return[Z,Buffer.from(i,"utf8").toString("base64url"),n.toString("base64url"),r.toString("base64url"),a.toString("base64url")].join(".")}function Ue(e,t){try{let[i,n,s,a,r]=e.split(".");if(i!==Z||!n||!s||!a||!r)return null;let o=Buffer.from(n,"base64url").toString("utf8");if(o!==t)return null;let d=ut("aes-256-gcm",_e(o),Buffer.from(s,"base64url"));return d.setAAD(Buffer.from(`${Z}:${o}`,"utf8")),d.setAuthTag(Buffer.from(a,"base64url")),Buffer.concat([d.update(Buffer.from(r,"base64url")),d.final()]).toString("utf8")}catch{return null}}function bi(e,t){return fe("sha256",_e(`identity:${e}`)).update(t.trim().toLowerCase()).digest("hex")}function hi(e,t,i,n){return fe("sha256",n).update(`${e}.${t}.${i}`).digest("hex")}function xi(e,t){if(!e||e.length!==t.length)return!1;try{return Nt(Buffer.from(e),Buffer.from(t))}catch{return!1}}var we="enhanced_matching",pt=90,Tt=10,H=class extends Error{constructor(i,n,s){super(n);this.code=i;this.status=s;this.name="AnalyticsIdentityProfileError"}code;status};function te(e,t){return e?.trim().replace(/\s+/g," ").slice(0,t)||null}function Lt(e){if(!e)return null;let[t,i]=e.split("@");return!t||!i?null:`${t.slice(0,1)}${"*".repeat(Math.min(6,Math.max(2,t.length-1)))}@${i}`}function At(e){if(!e)return null;let t=e.replace(/\D/g,"");return t.length<4?null:`***-***-${t.slice(-4)}`}function ue(e){return yt("sha256").update(e).digest("hex")}async function ie(e,t){if(!t.signals.length)return;let i=t.signals.map(a=>a.kind),n=t.signals.map(a=>a.valueHmac);if((await e.query(`SELECT 1 FROM analytics_identity_tombstones
2
+ WHERE site_id=$1 AND (kind,value_hmac) IN (
3
+ SELECT * FROM unnest($2::text[],$3::text[])
4
+ ) LIMIT 1`,[t.siteId,i,n])).rowCount)throw new H("analytics_identity_deleted","This identity cannot be automatically recreated.",410)}async function ne(e,t){let i=te(t.displayName,160),n=te(t.email,320)?.toLowerCase()??null,s=te(t.phone,40);if(!i&&!n&&!s)return!1;let r=(await e.query(`SELECT p.public_ref FROM analytics_people p
5
+ JOIN analytics_identity_namespaces n ON n.id=p.namespace_id AND n.site_id=p.site_id AND n.status='approved'
6
+ WHERE p.id=$1 AND p.site_id=$2`,[t.personId,t.siteId])).rows[0]?.public_ref;if(!r)return!1;let o=[...new Set([r,...t.consentSubjectRefs].filter(Boolean))],_=(await e.query(`SELECT s.latest_receipt_id::text FROM analytics_consent_current_state s
7
+ JOIN analytics_consent_receipts r ON r.id=s.latest_receipt_id AND r.site_id=s.site_id
8
+ WHERE s.site_id=$1 AND s.subject_ref=ANY($2::text[])
9
+ AND s.site_analytics=true AND s.enhanced_matching=true AND s.gpc=false
10
+ AND r.action NOT IN ('withdraw','gpc')
11
+ AND (s.expires_at IS NULL OR s.expires_at > now())
12
+ ORDER BY s.effective_at DESC LIMIT 1`,[t.siteId,o])).rows[0]?.latest_receipt_id;if(!_)return!1;let u=t.verifiedAt&&Number.isFinite(Date.parse(t.verifiedAt))?new Date(t.verifiedAt).toISOString():new Date().toISOString(),c=De(JSON.stringify({schemaVersion:1,displayName:i,email:n,phone:s,evidenceType:t.evidenceType,verifiedAt:u}),"identity-profile");return await e.query(`INSERT INTO analytics_restricted_data(id,site_id,person_id,purpose,subject_ref,payload_ciphertext,expires_at,consent_receipt_id)
13
+ VALUES($1,$2,$3,$4,$5,$6,now()+($7::int * interval '1 day'),$8)
14
+ ON CONFLICT(site_id,purpose,subject_ref) DO UPDATE SET
15
+ person_id=EXCLUDED.person_id,payload_ciphertext=EXCLUDED.payload_ciphertext,
16
+ expires_at=EXCLUDED.expires_at,consent_receipt_id=EXCLUDED.consent_receipt_id,created_at=now()`,[ee(),t.siteId,t.personId,we,r,c,pt,_]),!0}async function Mi(e,t){let i=t.now??new Date,n=await e.query("SELECT role FROM analytics_site_grants WHERE site_id=$1 AND user_id=$2",[t.siteId,t.actorUserId]);if(!n.rowCount)throw new H("analytics_site_not_found","Site not found.",404);if(!["owner","editor"].includes(n.rows[0].role))throw new H("analytics_forbidden","Editor access is required.",403);let s=await e.query(`SELECT count(*)::text AS count FROM analytics_identity_profile_access_audits
17
+ WHERE site_id=$1 AND actor_user_id=$2 AND created_at > $3::timestamptz - interval '1 minute'`,[t.siteId,t.actorUserId,i.toISOString()]);if(Number(s.rows[0]?.count??0)>=Tt)throw await e.query(`INSERT INTO analytics_identity_profile_access_audits(id,site_id,actor_user_id,subject_ref_hmac,purpose,result,created_at)
18
+ VALUES($1,$2,$3,$4,'journey_identity_display','rate_limited',$5)`,[ee(),t.siteId,t.actorUserId,ue(t.requestedPersonRef),i.toISOString()]),new H("analytics_identity_profile_rate_limited","Identity profile rate limit exceeded.",429);let a=async c=>{throw await e.query(`INSERT INTO analytics_identity_profile_access_audits(id,site_id,actor_user_id,subject_ref_hmac,purpose,result,created_at)
19
+ VALUES($1,$2,$3,$4,'journey_identity_display',$5,$6)`,[ee(),t.siteId,t.actorUserId,ue(t.requestedPersonRef),c,i.toISOString()]),new H("analytics_identity_profile_not_found","Identity profile not found.",404)};if(!t.personId)return a("not_found");let r=await e.query(`SELECT p.public_ref,r.payload_ciphertext,r.expires_at
20
+ FROM analytics_people p
21
+ JOIN analytics_restricted_data r ON r.person_id=p.id AND r.site_id=p.site_id
22
+ AND r.purpose=$3 AND r.subject_ref=p.public_ref AND r.expires_at>$4
23
+ JOIN analytics_consent_current_state s ON s.site_id=p.site_id AND s.latest_receipt_id=r.consent_receipt_id
24
+ AND s.site_analytics=true AND s.enhanced_matching=true AND s.gpc=false
25
+ AND (s.expires_at IS NULL OR s.expires_at>$4)
26
+ JOIN analytics_consent_receipts c ON c.id=s.latest_receipt_id AND c.site_id=s.site_id
27
+ AND c.action NOT IN ('withdraw','gpc')
28
+ WHERE p.id=$1 AND p.site_id=$2
29
+ AND NOT EXISTS (SELECT 1 FROM analytics_identity_tombstones t WHERE t.site_id=p.site_id AND t.person_id=p.id)
30
+ LIMIT 1`,[t.personId,t.siteId,we,i.toISOString()]);if(!r.rowCount)return a("not_found");let o=Ue(r.rows[0].payload_ciphertext,"identity-profile");if(!o)return a("unavailable");let d;try{d=JSON.parse(o)}catch{return a("unavailable")}let _=d.evidenceType;if(!["first_party_form","authenticated_user","crm_contact"].includes(String(_)))return a("unavailable");let u={personRef:t.requestedPersonRef,displayName:te(typeof d.displayName=="string"?d.displayName:void 0,160),maskedEmail:Lt(typeof d.email=="string"?d.email:null),maskedPhone:At(typeof d.phone=="string"?d.phone:null),evidenceType:_,verifiedAt:typeof d.verifiedAt=="string"&&Number.isFinite(Date.parse(d.verifiedAt))?new Date(d.verifiedAt).toISOString():i.toISOString()};return await e.query(`INSERT INTO analytics_identity_profile_access_audits(id,site_id,person_id,actor_user_id,subject_ref_hmac,purpose,result,created_at)
31
+ VALUES($1,$2,$3,$4,$5,'journey_identity_display','returned_masked',$6)`,[ee(),t.siteId,t.personId,t.actorUserId,ue(t.requestedPersonRef),i.toISOString()]),u}var It=new Set(["email","website","app","phone_call","chat","physical_store","system_generated","business_messaging","other"]);function f(e){return e&&typeof e=="object"&&!Array.isArray(e)?e:{}}function p(e,t=2048){return typeof e=="string"&&e.trim()?e.trim().slice(0,t):void 0}function gt(e){if(typeof e=="number"&&Number.isFinite(e)&&e>0)return Math.trunc(e>1e10?e/1e3:e);if(typeof e=="string"){let t=Date.parse(e);if(Number.isFinite(t)&&t>0)return Math.trunc(t/1e3)}return Math.trunc(Date.now()/1e3)}function vt(e){if(typeof e=="number"&&Number.isFinite(e)&&e>0)return new Date(e<1e10?e*1e3:e).toISOString();if(typeof e=="string"){let t=Date.parse(e);if(Number.isFinite(t)&&t>0)return new Date(t).toISOString()}throw new S("analytics_provider_destination_required","Google Data Manager requires a valid eventTimestamp.")}function be(e,t){if(typeof e=="number"&&Number.isFinite(e)&&e>0)return Math.trunc(e<1e10?e*1e3:e);if(typeof e=="string"){let i=Date.parse(e);if(Number.isFinite(i)&&i>0)return Math.trunc(i)}throw new S("analytics_provider_destination_required",`${t} requires a valid event timestamp.`)}function K(e){let t=p(e,64)?.toLowerCase();return t&&/^[a-f0-9]{64}$/.test(t)?t:void 0}function Ot(e){let t=f(e.consent),i=t.adUserData==="granted"?"CONSENT_GRANTED":t.adUserData==="denied"?"CONSENT_DENIED":void 0,n=t.adPersonalization==="granted"?"CONSENT_GRANTED":t.adPersonalization==="denied"?"CONSENT_DENIED":void 0;return i?{adUserData:i,...n?{adPersonalization:n}:{}}:void 0}function St(e,t){let i=p(t.operatingAccountId??t.googleAdsCustomerId??t.customerId,256);if(!i)throw new S("analytics_provider_destination_required","A Google Ads operating account ID is required for Data Manager activation.");let n=p(t.transactionId??t.orderId??t.eventId,256);if(!n)throw new S("analytics_provider_destination_required","A stable transactionId, orderId, or eventId is required for Google deduplication.");let s=f(t.clickIds),a=f(t.match),r=p(s.gclid,2e3),o=p(s.gbraid,2e3),d=p(s.wbraid,2e3),_=p(a.emailSha256,64),u=p(a.phoneSha256,64);if(!r&&!o&&!d&&!_&&!u)throw new S("analytics_provider_destination_required","Google Data Manager requires a click identifier or normalized SHA-256 user identifier.");let c=typeof t.valueMinor=="number"&&Number.isSafeInteger(t.valueMinor)&&t.valueMinor>=0?t.valueMinor:void 0,E=p(t.currency,3)?.toUpperCase(),N=p(t.actionSource,64),A=Ot(t);return{operatingAccountId:i,...p(t.loginAccountId,256)?{loginAccountId:p(t.loginAccountId,256)}:{},conversionActionId:e,events:[{eventTimestamp:vt(t.eventTime??t.eventTimestamp),transactionId:n,eventSource:N==="phone_call"?"PHONE":N==="app"?"APP":"WEB",...r?{gclid:r}:{},...o?{gbraid:o}:{},...d?{wbraid:d}:{},..._?{emailSha256:[_]}:{},...u?{phoneSha256:[u]}:{},...c!==void 0&&E?{conversionValue:c/100,currency:E}:{},...A?{consent:A}:{}}],...A?{consent:A}:{},validateOnly:!1}}function he(e){let t=f(e.consent),i=p(e.networkDataRetentionUntil,64),n=i?Date.parse(i):Number.NaN;return t.analytics==="granted"&&t.adUserData==="granted"&&t.globalPrivacyControl!==!0&&Number.isFinite(n)&&n>Date.now()}function Ct(e){let t=f(e.match),i=f(e.clickIds),n=p(t.emailSha256,64),s=p(t.phoneSha256,64),a=p(i.fbp,255),r=p(i.fbc,255),o={...n?{em:[n]}:{},...s?{ph:[s]}:{},...a?{fbp:a}:{},...r?{fbc:r}:{}};if(he(e)){let d=p(t.clientIpAddress,64),_=p(t.clientUserAgent,1e3);d&&(o.client_ip_address=d),_&&(o.client_user_agent=_)}return o}function Rt(e,t,i){let n=p(i.actionSource,64),s=typeof i.valueMinor=="number"&&Number.isSafeInteger(i.valueMinor)?i.valueMinor:void 0,a=p(i.currency,3)?.toUpperCase(),r=p(i.orderId,240),o={...s!==void 0&&s>=0?{value:s/100}:{},...a?{currency:a}:{},...r?{order_id:r}:{}},d=p(i.eventId,255);if(!d)throw new S("analytics_provider_destination_required","Meta requires the stable X-Ray conversion ID for event deduplication.");let _=p(i.sourceUrl),u={eventName:t,eventTime:gt(i.eventTime),actionSource:n&&It.has(n)?n:"system_generated",eventId:d,..._?{eventSourceUrl:_}:{},userData:Ct(i),...Object.keys(o).length?{customData:o}:{}},c=p(i.testEventCode,64);return{datasetId:e,events:[u],...c?{testEventCode:c}:{}}}function xe(e){return he(e)}function ft(e,t,i){let n=p(i.eventId,255);if(!n)throw new S("analytics_provider_destination_required","TikTok requires the stable X-Ray conversion ID for event deduplication.");let s=f(i.clickIds),a=f(i.match),r=K(a.emailSha256),o=K(a.phoneSha256),d=K(a.externalIdSha256),_=p(s.ttclid,2e3),u=p(s.ttp,255),c={...r?{emailSha256:[r]}:{},...o?{phoneSha256:[o]}:{},...d?{externalIdSha256:[d]}:{},..._?{ttclid:_}:{},...u?{ttp:u}:{}};if(xe(i)){let O=p(a.clientIpAddress,64),w=p(a.clientUserAgent,1e3);O&&(c.clientIpAddress=O),w&&(c.clientUserAgent=w)}if(!Object.keys(c).length)throw new S("analytics_provider_destination_required","TikTok requires ttclid, ttp, or a documented SHA-256 match field.");let E=Math.trunc(be(i.eventTime,"TikTok")/1e3),N=typeof i.valueMinor=="number"&&Number.isSafeInteger(i.valueMinor)&&i.valueMinor>=0?i.valueMinor:void 0,A=p(i.currency,3)?.toUpperCase(),v=p(i.sourceUrl,2048),R=p(i.referrer,2048),C=p(i.orderId,255),T=p(i.testEventCode,128);return{pixelCode:e,events:[{eventId:n,eventName:t,eventTime:E,userData:c,...v?{page:{url:v,...R?{referrer:R}:{}}}:{},...N!==void 0&&A?{properties:{value:N/100,currency:A,...C?{orderId:C}:{}}}:C?{properties:{orderId:C}}:{}}],...T?{testEventCode:T}:{},schemaValidationOnly:!1}}var Dt=new Map([["page_view","PAGE_VISIT"],["page_visit","PAGE_VISIT"],["view_content","VIEW_CONTENT"],["search","SEARCH"],["add_to_cart","ADD_TO_CART"],["add_to_wishlist","ADD_TO_WISHLIST"],["purchase","PURCHASE"],["lead","LEAD"],["qualified_lead","LEAD"],["sign_up","SIGN_UP"],["signup","SIGN_UP"]]);function Ut(e){let t=p(e,64);return t==="website"?"WEBSITE":t==="app"?"APP":t==="physical_store"?"PHYSICAL_STORE":"OTHER"}function wt(e,t,i){let n=p(i.eventId,255);if(!n)throw new S("analytics_provider_destination_required","Reddit requires the stable X-Ray conversion ID for event deduplication.");let s=f(i.clickIds),a=f(i.match),r=p(s.rdtCid??s.rdt_cid,2e3),o=K(a.emailSha256),d=K(a.phoneSha256),_=K(a.externalIdSha256),u=p(a.redditUuid??a.uuid,255),c={...o?{emailSha256:o}:{},...d?{phoneSha256:d}:{},..._?{externalIdSha256:_}:{},...u?{uuid:u}:{}};if(xe(i)){let O=p(a.clientIpAddress,64),w=p(a.clientUserAgent,1e3);O&&(c.ipAddress=O),w&&(c.userAgent=w)}if(!r&&!Object.keys(c).length)throw new S("analytics_provider_destination_required","Reddit requires rdt_cid or a documented user match field.");let E=t.trim().toLowerCase().replace(/[ -]+/g,"_"),N=Dt.get(E)??"CUSTOM",A=typeof i.valueMinor=="number"&&Number.isSafeInteger(i.valueMinor)&&i.valueMinor>=0?i.valueMinor:void 0,v=p(i.currency,3)?.toUpperCase(),R=typeof i.itemCount=="number"&&Number.isSafeInteger(i.itemCount)&&i.itemCount>=0?i.itemCount:void 0,C=p(i.sourceUrl,2048),T=p(i.testId,255);return{pixelId:e,events:[{conversionId:n,...r?{clickId:r}:{},eventAt:be(i.eventTime,"Reddit"),actionSource:Ut(i.actionSource),...C?{eventSourceUrl:C}:{},trackingType:N,...N==="CUSTOM"?{customEventName:t.slice(0,64)}:{},...A!==void 0&&v?{value:A/100,currency:v}:{},...R!==void 0?{itemCount:R}:{},user:c}],...T?{testId:T}:{},schemaValidationOnly:!1}}var S=class extends Error{code;constructor(t,i){super(i),this.name="AnalyticsProviderRegistryError",this.code=t}},se=Object.freeze({retryableStatusCodes:Object.freeze([408,425,429,500,502,503,504]),permanentStatusCodes:Object.freeze([400,401,403,404,409,422]),maxAttempts:8}),bt=Object.freeze({meta:Object.freeze({platform:"meta",integrationKeys:Object.freeze(["meta-marketing-api"]),actionName:"send-conversion-events",requiredDestinationField:"datasetId",supportsTestMode:!0,supportsDiagnostics:!1,maxBatchSize:1e3,retryClassification:se}),google:Object.freeze({platform:"google",integrationKeys:Object.freeze(["google-data-manager"]),actionName:"send-conversion-events",requiredDestinationField:"conversionActionId",supportsTestMode:!0,supportsDiagnostics:!0,diagnosticsActionName:"get-ingestion-diagnostics",maxBatchSize:1e3,retryClassification:se}),tiktok:Object.freeze({platform:"tiktok",integrationKeys:Object.freeze(["tiktok-business"]),actionName:"send-events",requiredDestinationField:"pixelCode",supportsTestMode:!0,supportsDiagnostics:!1,maxBatchSize:1e3,retryClassification:se}),reddit:Object.freeze({platform:"reddit",integrationKeys:Object.freeze(["reddit-ads"]),actionName:"send-conversion-events",requiredDestinationField:"pixelId",supportsTestMode:!0,supportsDiagnostics:!1,maxBatchSize:1e3,retryClassification:se})});function re(e){return bt[e]}function Ee(e,t){let i=t?.trim()??"";if(!i){let n=re(e);throw new S("analytics_provider_destination_required",`A ${n.requiredDestinationField} is required for this activation provider.`)}return i}function ae(e,t){return typeof e=="string"&&e.trim().length>0&&e.trim().length<=t&&/^[a-zA-Z][a-zA-Z0-9_.: -]*$/.test(e.trim())}function X(e){if(e&&Object.prototype.hasOwnProperty.call(e,"__tool"))throw new S("analytics_provider_action_unavailable","Activation action selection is managed by X-Ray.");if(!e)return{schemaVersion:1,mappings:[]};if("schemaVersion"in e||"mappings"in e){let i=e;if(i.schemaVersion!==1||!Array.isArray(i.mappings)||i.mappings.length>200)throw new S("analytics_provider_destination_required","Activation mappings must use the supported versioned mapping contract.");let n=i.mappings.map(a=>{if(!a||typeof a!="object"||!ae(a.source,160)||!ae(a.providerEvent,160)||typeof a.enabled!="boolean"||!["primary","observation"].includes(a.role)||!["none","event","fixed"].includes(a.valueMode))throw new S("analytics_provider_destination_required","Activation mapping contains an unsupported event, role, or value mode.");let r=a.currency?.trim().toUpperCase();if(a.valueMode==="fixed"&&(!Number.isFinite(a.fixedValue)||Number(a.fixedValue)<0||Number(a.fixedValue)>1e9||!r||!/^[A-Z]{3}$/.test(r)))throw new S("analytics_provider_destination_required","Fixed activation values require a non-negative value and ISO currency.");return{source:a.source.trim(),providerEvent:a.providerEvent.trim(),enabled:a.enabled,role:a.role,valueMode:a.valueMode,...a.valueMode==="fixed"?{fixedValue:Number(a.fixedValue),currency:r}:{}}}),s=n.filter(a=>a.enabled);if(s.length&&s.filter(a=>a.role==="primary").length!==1)throw new S("analytics_provider_destination_required","Enabled activation mappings require exactly one primary optimization event.");if(new Set(n.map(a=>a.source.toLowerCase())).size!==n.length)throw new S("analytics_provider_destination_required","Each X-Ray event may be mapped only once per destination.");return{schemaVersion:1,mappings:n}}return{schemaVersion:1,mappings:Object.entries(e).map(([i,n],s)=>{if(!ae(i,160)||!ae(n,160))throw new S("analytics_provider_destination_required","Activation mapping contains an invalid event name.");return{source:i.trim(),providerEvent:n.trim(),enabled:!0,role:s===0?"primary":"observation",valueMode:"event"}})}}function le(e){X(e)}function oe(e,t){let i=re(t);if(!Se(e,i.integrationKeys,i.actionName))throw new S("analytics_provider_action_unavailable","The selected connection cannot send conversions for this provider.");return i}function ht(e){let t=String(e.payload.eventName||"conversion");return X(e.eventMapping).mappings.find(n=>n.enabled&&n.source===t)?.providerEvent||t}function Fe(e,t){let i=X(e);return i.mappings.length===0||i.mappings.some(n=>n.enabled&&n.source===t)}function xt(e){let t=String(e.payload.eventName||"conversion"),i=X(e.eventMapping).mappings.find(s=>s.enabled&&s.source===t);if(!i||i.valueMode==="event")return e.payload;let n={...e.payload};return i.valueMode==="none"?(delete n.valueMinor,delete n.currency,n):(n.valueMinor=Math.round((i.fixedValue??0)*100),n.currency=i.currency,n)}function Ft(e){let t=Ee(e.platform,e.destinationId);le(e.eventMapping);let i=ht(e),n=xt(e);return e.platform==="meta"?Rt(t,i,n):e.platform==="google"?St(t,n):e.platform==="tiktok"?ft(t,i,n):wt(t,i,n)}function $e(e){let t=re(e.platform),i=Ft(e);return e.platform==="google"?{...i,validateOnly:!0}:e.platform==="tiktok"?p(e.payload.testEventCode,128)?i:{...i,schemaValidationOnly:!0}:e.platform==="reddit"?p(e.payload.testId,255)?i:{...i,schemaValidationOnly:!0}:t.supportsTestMode?{...i,testMode:!0}:i}function Y(e,...t){for(let i of t){let n=e[i];if(typeof n=="string"&&n.trim())return n.trim().slice(0,500)}}function $t(e){let t=f(e),i=f(t.structuredContent);if(Object.keys(i).length>0)return i;let n=Array.isArray(t.content)?t.content:[];for(let s of n){let a=f(s);if(!(a.type!=="text"||typeof a.text!="string"||a.text.length>1e6))try{let r=JSON.parse(a.text),o=f(r);if(Object.keys(o).length>0)return o}catch{}}return t}function qe(e,t,i){re(e);let n=$t(t),s=[n.warnings,n.messages,n.fieldWarnings].flatMap(E=>Array.isArray(E)?E:[]).map(E=>{if(typeof E=="string")return E;let N=f(E),A=p(N.reason,200),v=p(N.field,200),R=p(N.description,500);return[A,v,R].filter(Boolean).join(": ")}).filter(E=>!!E).slice(0,50).map(E=>E.slice(0,500)),a=typeof n.eventsReceived=="number"&&Number.isFinite(n.eventsReceived)?Math.max(0,Math.trunc(n.eventsReceived)):void 0,r=typeof n.eventsRejected=="number"&&Number.isFinite(n.eventsRejected)?Math.max(0,Math.trunc(n.eventsRejected)):void 0,o=n.partiallyAccepted===!0,d=Array.isArray(n.errors)?n.errors:[],_=e==="google"&&(/PARTIAL_SUCCESS/i.test(String(n.status??""))||o)&&(d.length>0||(r??0)>0),u=e==="google"&&n.acceptedForProcessing===!0&&!!Y(n,"requestId")&&!_,c=n.accepted===!0||n.success===!0||n.providerAccepted===!0||(a??0)>0;return{accepted:n.schemaValidationOnly!==!0&&!_&&(e==="google"?c&&!u:c),...u?{pending:!0}:{},...o?{partiallyAccepted:!0}:{},...Y(n,"requestId","receiptId","fbtraceId","fbtrace_id","id")?{requestId:Y(n,"requestId","receiptId","fbtraceId","fbtrace_id","id")}:{},...Y(n,"eventId","conversionId")||p(i,255)?{eventId:Y(n,"eventId","conversionId")||p(i,255)}:{},...a!==void 0?{eventsReceived:a}:{},...r!==void 0?{eventsRejected:r}:{},warnings:[...s,...d.map(E=>{let N=f(E);return[p(N.reason,200),p(N.field,200),p(N.description,500)].filter(Boolean).join(": ")||"provider_error"})].slice(0,50)}}var qt=["first_touch","last_touch","last_non_direct","linear","time_decay","position_based","custom_weighted"];var Me=[7,14,30,60,90,180,365,"lifetime"],U=class extends Error{constructor(i,n){super(n);this.code=i;this.name="AnalyticsAttributionReportError"}code};function Mt(e){let t=e==="position_40_20_40"?"position_based":e;if(!qt.includes(t))throw new U("invalid_model",`Unsupported attribution model: ${e}.`);return t}function x(e,t){let i=e instanceof Date?e.getTime():Date.parse(e);if(!Number.isFinite(i))throw new U("invalid_timestamp",`${t} must be a valid timestamp.`);return i}function je(e,t){if(e==null)return 0n;try{let i=typeof e=="bigint"?e:BigInt(e);if(i<0n)throw new Error("negative");return i}catch{throw new U("invalid_minor_units",`${t} must be a non-negative integer.`)}}function Ke(e){if(e==null||e.trim()==="")return null;let t=e.trim().toUpperCase();if(!/^[A-Z]{3}$/.test(t))throw new U("invalid_currency",`Invalid currency ${e}.`);return t}function kt(e){return e.touchKind==="direct"||e.source?.trim().toLowerCase()==="(direct)"&&["(none)","none",""].includes(e.medium?.trim().toLowerCase()??"")}function Ht(e,t){return e.touchKind==="view"?t.viewWindow??90:t.clickWindow??90}function Pt(e,t,i){let n=x(t.occurredAt,"conversion.occurredAt");return e.filter(s=>s.subjectRef===t.subjectRef&&s.journeyTier===t.journeyTier).filter(s=>{let a=x(s.occurredAt,"touch.occurredAt");if(a>n)return!1;let r=Ht(s,i);return r==="lifetime"||n-a<=r*864e5}).sort((s,a)=>x(s.occurredAt,"touch.occurredAt")-x(a.occurredAt,"touch.occurredAt")||s.touchId.localeCompare(a.touchId))}function jt(e,t){let i=[`touch:${e.touchId}`,e.adId?`ad:${e.adId}`:null,e.adSetId?`ad_set:${e.adSetId}`:null,e.campaignId?`campaign:${e.campaignId}`:null,e.platform?`platform:${e.platform}`:null,e.source?`source:${e.source}`:null,e.medium?`medium:${e.medium}`:null,`touch_kind:${e.touchKind}`,"default"];for(let n of i)if(n&&t[n]!=null)return t[n];return 0}function Kt(e,t){if(e.length===0)return[];if(t.model==="first_touch")return e.map((s,a)=>a===0?1:0);if(t.model==="last_touch")return e.map((s,a)=>a===e.length-1?1:0);if(t.model==="last_non_direct"){let s=-1;for(let a=e.length-1;a>=0;a-=1)if(!kt(e[a])){s=a;break}return s===-1&&(s=e.length-1),e.map((a,r)=>r===s?1:0)}if(t.model==="linear")return e.map(()=>1);if(t.model==="time_decay"){let s=t.timeDecayHalfLifeDays??7;if(!Number.isFinite(s)||s<=0)throw new U("invalid_half_life","Time-decay half-life must be positive.");let a=Math.max(...e.map(r=>x(r.occurredAt,"touch.occurredAt")));return e.map(r=>Math.pow(.5,(a-x(r.occurredAt,"touch.occurredAt"))/864e5/s))}if(t.model==="position_based"||t.model==="position_40_20_40")return e.length===1?[1]:e.length===2?[.5,.5]:e.map((s,a)=>a===0||a===e.length-1?.4:.2/(e.length-2));let i=t.customWeights;if(!i||Object.keys(i).length===0)throw new U("custom_weights_required","Custom-weighted attribution requires at least one selector.");for(let[s,a]of Object.entries(i))if(!s.trim()||!Number.isFinite(a)||a<0)throw new U("invalid_custom_weight",`Invalid custom weight for ${s||"(empty)"}.`);let n=new Set(["first","middle","last"]);if(Object.keys(i).every(s=>n.has(s))){let s=i.first??0,a=i.middle??0,r=i.last??0;return e.length===1?[s+a+r]:e.length===2?[s+a/2,r+a/2]:e.map((o,d)=>d===0?s:d===e.length-1?r:a/(e.length-2))}return e.map(s=>jt(s,i))}function Xt(e){if(e.length===0)return[];let t=e.reduce((r,o)=>r+o,0);if(!Number.isFinite(t)||t<=0)throw new U("zero_attribution_weight","Eligible touches must have a positive total weight.");let i=e.map(r=>r/t*1e6),n=i.map(r=>Math.floor(r)),s=1e6-n.reduce((r,o)=>r+o,0),a=i.map((r,o)=>({index:o,fraction:r-n[o]})).sort((r,o)=>o.fraction-r.fraction||r.index-o.index);for(let r=0;r<s;r+=1)n[a[r%a.length].index]+=1;return n}function Bt(e,t){if(t.length===0)return[];let i=BigInt(1e6),n=t.map(o=>e*BigInt(o)/i),s=e-n.reduce((o,d)=>o+d,0n),a=t.map((o,d)=>({index:d,remainder:e*BigInt(o)%i})).sort((o,d)=>o.remainder===d.remainder?o.index-d.index:o.remainder>d.remainder?-1:1),r=0;for(;s>0n;)n[a[r%a.length].index]+=1n,s-=1n,r+=1;return n}function W(e,t){let i={platform:e.platform?.trim()||null};return t==="platform"||(i.accountId=e.accountId?.trim()||null,t==="account")||(i.campaignId=e.campaignId?.trim()||null,i.campaignName=e.campaignName?.trim()||null,t==="campaign")||(i.adSetId=e.adSetId?.trim()||null,i.adSetName=e.adSetName?.trim()||null,t==="ad_set")||(i.adId=e.adId?.trim()||null,i.adName=e.adName?.trim()||null),i}function ke(e,t){let i=W(e,t);return[i.platform,i.accountId,i.campaignId,i.adSetId,i.adId].filter((n,s)=>s<=["platform","account","campaign","ad_set","ad"].indexOf(t)).map(n=>n||"(missing)").join("|")}function zt(e,t){let i=W(e,t);return!i.platform||t!=="platform"&&!i.accountId||["campaign","ad_set","ad"].includes(t)&&!i.campaignId||["ad_set","ad"].includes(t)&&!i.adSetId?!0:t==="ad"&&!i.adId}function He(e,t,i){let n=[];for(let s of e){let a=Pt(t,s,i),r=Ke(s.currency),o=je(s.valueMinor,"conversion.valueMinor");if(a.length===0){n.push({conversionId:s.conversionId,touchId:null,subjectRef:s.subjectRef,journeyTier:s.journeyTier,occurredAt:new Date(x(s.occurredAt,"conversion.occurredAt")).toISOString(),outcome:s.outcome,creditMicros:1e6,creditedValueMinor:o.toString(),currency:r,unattributed:!0});continue}let d=Xt(Kt(a,i)),_=Bt(o,d);a.forEach((u,c)=>{d[c]!==0&&n.push({...W(u,"ad"),conversionId:s.conversionId,touchId:u.touchId,subjectRef:s.subjectRef,journeyTier:s.journeyTier,occurredAt:new Date(x(s.occurredAt,"conversion.occurredAt")).toISOString(),outcome:s.outcome,creditMicros:d[c],creditedValueMinor:_[c].toString(),currency:r,unattributed:!1})})}return n}function Pe(e,t,i){let n=new Map,s=(_,u,c)=>{let E=n.get(_);return E||(E={dimensions:u,unattributed:c,leadMicros:0,qualifiedMicros:0,conversionMicros:0,customerMicros:0,revenueMinor:0n,spendMinor:0n,currencies:new Set,spendFacts:0,matchedSpendFacts:0,missingDimension:zt(u,i),hasCredits:!1},n.set(_,E)),E},a=0;for(let _ of e){let u=_.unattributed?{}:W(_,i),c=_.unattributed?"(unattributed)":ke(u,i),E=s(c,u,_.unattributed);E.hasCredits=!0,!_.unattributed&&E.missingDimension&&(a+=1),_.outcome==="lead"&&(E.leadMicros+=_.creditMicros),_.outcome==="qualified"&&(E.qualifiedMicros+=_.creditMicros),_.outcome==="conversion"&&(E.conversionMicros+=_.creditMicros),_.outcome==="customer"&&(E.customerMicros+=_.creditMicros),E.revenueMinor+=BigInt(_.creditedValueMinor),_.currency&&E.currencies.add(_.currency)}let r=0;for(let _ of t){let u=W(_,i),c=ke(u,i),E=s(c,u,!1);E.spendMinor+=je(_.spendMinor,"spendFact.spendMinor"),E.currencies.add(Ke(_.currency)),E.spendFacts+=1,E.hasCredits&&!E.missingDimension?E.matchedSpendFacts+=1:r+=1}let o=new Set;for(let _ of n.values())for(let u of _.currencies)o.add(u);if(o.size>1)throw new U("mixed_currencies",`Attribution reports require one currency; received ${[...o].sort().join(", ")}.`);return{rows:[...n.entries()].map(([_,u])=>{let c=u.leadMicros/1e6,E=u.qualifiedMicros/1e6,N=u.conversionMicros/1e6,A=u.customerMicros/1e6,v=Number(u.spendMinor),R=Number(u.revenueMinor),C=u.spendFacts>0,T=O=>C&&O>0?v/O:null;return{...u.dimensions,key:_,unattributed:u.unattributed,leadCredit:c,qualifiedCredit:E,conversionCredit:N,customerCredit:A,spendMinor:C?u.spendMinor.toString():null,revenueMinor:u.hasCredits&&u.currencies.size>0?u.revenueMinor.toString():null,currency:u.currencies.size===1?[...u.currencies][0]:null,cplMinor:T(c),cpqlMinor:T(E),cpaMinor:T(N),cacMinor:T(A),roas:C&&v>0&&u.hasCredits?R/v:null,coverage:{spendFacts:u.spendFacts,matchedSpendFacts:u.matchedSpendFacts,missingDimension:u.missingDimension}}}).sort((_,u)=>_.key.localeCompare(u.key)),unmatchedSpendFactCount:r,missingDimensionCredits:a}}function Ne(e,t){let i=new Set;for(let n of e){if(!n.id||i.has(n.id))throw new U(`invalid_${t}_id`,`${t} IDs must be present and unique.`);i.add(n.id)}}function me(e){let t=Mt(e.configuration.model),i=e.configuration.clickWindow??90,n=e.configuration.viewWindow??90;if(!Me.includes(i)||!Me.includes(n))throw new U("invalid_window","Attribution window must be 7, 14, 30, 60, 90, 180, 365 or lifetime.");Ne(e.touches.map(E=>({id:E.touchId})),"touch"),Ne(e.conversions.map(E=>({id:E.conversionId})),"conversion"),Ne((e.spendFacts??[]).map(E=>({id:E.spendFactId})),"spend_fact");let s={...e.configuration,model:t,clickWindow:i,viewWindow:n},a=e.conversions.filter(E=>E.journeyTier==="confirmed"),r=e.conversions.filter(E=>E.journeyTier==="candidate"&&E.candidateConfidence==="high"),o=He(a,e.touches.filter(E=>E.journeyTier==="confirmed"),s),d=He(r,e.touches.filter(E=>E.journeyTier==="candidate"&&E.candidateConfidence==="high"),s),_=e.granularity??"campaign",u=Pe(o,e.spendFacts??[],_),c=Pe(d,[],_);return{configuration:{id:e.configuration.id??`${t}:${i}:${n}`,model:t,clickWindow:i,viewWindow:n,timeDecayHalfLifeDays:e.configuration.timeDecayHalfLifeDays??7},confirmed:{credits:o,rows:u.rows,conversionCount:a.length,unattributedCount:new Set(o.filter(E=>E.unattributed).map(E=>E.conversionId)).size},candidateAssist:{credits:d,rows:c.rows,conversionCount:r.length,unattributedCount:new Set(d.filter(E=>E.unattributed).map(E=>E.conversionId)).size,eligibility:"high_confidence_only"},coverage:{touchCount:e.touches.length,conversionCount:e.conversions.length,excludedCandidateConversions:e.conversions.filter(E=>E.journeyTier==="candidate"&&E.candidateConfidence!=="high").length,spendFactCount:e.spendFacts?.length??0,unmatchedSpendFactCount:u.unmatchedSpendFactCount,missingDimensionCredits:u.missingDimensionCredits},providerComparison:e.providerRead?{availability:"available",provider:e.providerRead.provider,conversionCount:e.providerRead.conversionCount,sourceRef:e.providerRead.sourceRef}:{availability:"unavailable"}}}function Xe(e){if(e.configurations.length<1||e.configurations.length>3)throw new U("invalid_comparison_count","Compare between one and three attribution configurations.");let t=e.configurations.map((i,n)=>i.id??`${i.model}:${i.clickWindow??90}:${i.viewWindow??90}:${n}`);if(new Set(t).size!==t.length)throw new U("duplicate_comparison_id","Comparison configuration IDs must be unique.");return{evidence:{touchIds:e.touches.map(i=>i.touchId).sort(),conversionIds:e.conversions.map(i=>i.conversionId).sort(),spendFactIds:(e.spendFacts??[]).map(i=>i.spendFactId).sort()},projections:e.configurations.map((i,n)=>me({...e,configuration:{...i,id:t[n]}}))}}async function Be(e){let t=new Date(x(e.from,"from")).toISOString(),i=new Date(x(e.to,"to")).toISOString(),n=await e.client.query(`SELECT id, person_id, occurred_at, conversion_kind, value_minor::text, currency
32
+ FROM analytics_conversions
33
+ WHERE site_id = $1 AND person_id IS NOT NULL AND occurred_at >= $2 AND occurred_at < $3
34
+ ORDER BY occurred_at, id`,[e.siteId,t,i]),s=await e.client.query(`SELECT id, person_id, occurred_at, touch_kind, source, medium, platform, account_id, campaign_id, campaign, ad_set_id, ad_id
35
+ FROM analytics_attribution_touches
36
+ WHERE site_id = $1 AND person_id IS NOT NULL AND occurred_at < $2
37
+ ORDER BY occurred_at, id`,[e.siteId,i]),a=await e.client.query(`SELECT id, bucket, platform, account_id, campaign_id, campaign_name, ad_set_id, ad_set_name, ad_id, ad_name, spend_minor::text, currency, source_ref
38
+ FROM analytics_ad_spend_facts
39
+ WHERE site_id = $1 AND bucket >= $2::date AND bucket < $3::date
40
+ ORDER BY bucket, id`,[e.siteId,t,i]);return{conversions:n.rows.map(r=>({conversionId:r.id,subjectRef:r.person_id,journeyTier:"confirmed",occurredAt:r.occurred_at,outcome:["lead","qualified","conversion","customer"].includes(r.conversion_kind)?r.conversion_kind:"conversion",valueMinor:r.value_minor,currency:r.currency})),touches:s.rows.map(r=>({touchId:r.id,subjectRef:r.person_id,journeyTier:"confirmed",occurredAt:r.occurred_at,touchKind:r.touch_kind,source:r.source,medium:r.medium,platform:r.platform,accountId:r.account_id,campaignId:r.campaign_id,campaignName:r.campaign,adSetId:r.ad_set_id,adId:r.ad_id})),spendFacts:a.rows.map(r=>({spendFactId:r.id,bucket:r.bucket,platform:r.platform,accountId:r.account_id,campaignId:r.campaign_id,campaignName:r.campaign_name,adSetId:r.ad_set_id,adSetName:r.ad_set_name,adId:r.ad_id,adName:r.ad_name,spendMinor:r.spend_minor,currency:r.currency,sourceRef:r.source_ref}))}}async function ze(e){let t=new Date(x(e.from,"from")).toISOString(),i=new Date(x(e.to,"to")).toISOString(),n=await e.client.query(`SELECT t.id, cat.association_id, cat.association_confidence, t.occurred_at, t.touch_kind,
41
+ t.source, t.medium, t.platform, t.account_id, t.campaign_id, t.campaign, t.ad_set_id, t.ad_id
42
+ FROM analytics_candidate_attribution_touches cat
43
+ JOIN analytics_candidate_associations ca ON ca.id=cat.association_id AND ca.namespace_id=cat.namespace_id
44
+ JOIN analytics_attribution_touches t ON t.id=cat.touch_id AND t.site_id=cat.site_id
45
+ WHERE cat.site_id=$1 AND cat.eligibility='overlay_only' AND cat.expires_at > now()
46
+ AND ca.status='active' AND ca.confidence_band='high' AND ca.expires_at > now()
47
+ AND t.occurred_at < $2
48
+ ORDER BY t.occurred_at,t.id`,[e.siteId,i]),s=await e.client.query(`SELECT DISTINCT ON (c.id) c.id, cat.association_id, cat.association_confidence, c.occurred_at,
49
+ c.conversion_kind, c.value_minor::text, c.currency
50
+ FROM analytics_candidate_attribution_touches cat
51
+ JOIN analytics_candidate_associations ca ON ca.id=cat.association_id AND ca.namespace_id=cat.namespace_id
52
+ JOIN analytics_attribution_touches t ON t.id=cat.touch_id AND t.site_id=cat.site_id
53
+ JOIN analytics_sessions s ON s.id=t.session_id AND s.site_id=t.site_id
54
+ JOIN analytics_conversions c ON c.site_id=cat.site_id AND c.session_hmac=s.session_hmac
55
+ WHERE cat.site_id=$1 AND cat.eligibility='overlay_only' AND cat.expires_at > now()
56
+ AND ca.status='active' AND ca.confidence_band='high' AND ca.expires_at > now()
57
+ AND c.occurred_at >= $2 AND c.occurred_at < $3
58
+ ORDER BY c.id,cat.association_confidence DESC,cat.association_id`,[e.siteId,t,i]);return{touches:n.rows.map(a=>({touchId:`candidate:${a.association_id}:${a.id}`,subjectRef:`candidate:${a.association_id}`,journeyTier:"candidate",candidateConfidence:"high",occurredAt:a.occurred_at,touchKind:a.touch_kind,source:a.source,medium:a.medium,platform:a.platform,accountId:a.account_id,campaignId:a.campaign_id,campaignName:a.campaign,adSetId:a.ad_set_id,adId:a.ad_id})),conversions:s.rows.map(a=>({conversionId:`candidate:${a.association_id}:${a.id}`,subjectRef:`candidate:${a.association_id}`,journeyTier:"candidate",candidateConfidence:"high",occurredAt:a.occurred_at,outcome:["lead","qualified","conversion","customer"].includes(a.conversion_kind)?a.conversion_kind:"conversion",valueMinor:a.value_minor,currency:a.currency})),spendFacts:[]}}var m=class extends Error{constructor(i,n,s=400){super(n);this.code=i;this.status=s;this.name="AnalyticsRepositoryError"}code;status},B=null;function Gt(){let e=process.env.ANALYTICS_DATABASE_URL?.trim();if(!e)throw new m("analytics_database_unavailable","Site Analytics is not configured.",503);return e}function l(){return B||(B=new Wt({connectionString:Gt(),max:8})),B}async function Vi(){if(!B)return;let e=B;B=null,await e.end()}async function Ji(){let t=await l().connect();try{await t.query(`
59
+ CREATE TABLE IF NOT EXISTS analytics_schema_migrations (
60
+ version text PRIMARY KEY,
61
+ applied_at timestamptz NOT NULL DEFAULT now()
62
+ )
63
+ `),await t.query(`
64
+ CREATE TABLE IF NOT EXISTS analytics_sites (
65
+ id uuid PRIMARY KEY,
66
+ slug text NOT NULL,
67
+ name text NOT NULL,
68
+ owner_user_id bigint NOT NULL,
69
+ timezone text NOT NULL DEFAULT 'UTC',
70
+ status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'disabled')),
71
+ created_at timestamptz NOT NULL DEFAULT now(),
72
+ updated_at timestamptz NOT NULL DEFAULT now(),
73
+ UNIQUE(owner_user_id, slug)
74
+ )
75
+ `),await t.query(`
76
+ CREATE TABLE IF NOT EXISTS analytics_site_grants (
77
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
78
+ user_id bigint NOT NULL,
79
+ role text NOT NULL CHECK (role IN ('viewer', 'editor', 'owner')),
80
+ created_at timestamptz NOT NULL DEFAULT now(),
81
+ PRIMARY KEY(site_id, user_id)
82
+ )
83
+ `),await t.query(`
84
+ CREATE TABLE IF NOT EXISTS analytics_pixels (
85
+ id uuid PRIMARY KEY,
86
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
87
+ public_id text NOT NULL UNIQUE,
88
+ name text NOT NULL,
89
+ environment text NOT NULL DEFAULT 'production',
90
+ status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'disabled')),
91
+ created_at timestamptz NOT NULL DEFAULT now(),
92
+ updated_at timestamptz NOT NULL DEFAULT now(),
93
+ disabled_at timestamptz
94
+ )
95
+ `),await t.query(`
96
+ CREATE TABLE IF NOT EXISTS analytics_pixel_domains (
97
+ id uuid PRIMARY KEY,
98
+ pixel_id uuid NOT NULL REFERENCES analytics_pixels(id) ON DELETE CASCADE,
99
+ hostname text NOT NULL,
100
+ state text NOT NULL DEFAULT 'observed_unverified'
101
+ CHECK (state IN ('observed_unverified', 'approved', 'rejected', 'revoked')),
102
+ first_seen_at timestamptz NOT NULL DEFAULT now(),
103
+ last_seen_at timestamptz NOT NULL DEFAULT now(),
104
+ accepted_count bigint NOT NULL DEFAULT 0,
105
+ rejected_count bigint NOT NULL DEFAULT 0,
106
+ updated_at timestamptz NOT NULL DEFAULT now(),
107
+ UNIQUE(pixel_id, hostname)
108
+ )
109
+ `),await t.query(`
110
+ CREATE TABLE IF NOT EXISTS analytics_host_groups (
111
+ id uuid PRIMARY KEY,
112
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
113
+ pixel_id uuid NOT NULL REFERENCES analytics_pixels(id) ON DELETE CASCADE,
114
+ name text NOT NULL,
115
+ auto_decorate boolean NOT NULL DEFAULT false,
116
+ created_at timestamptz NOT NULL DEFAULT now(),
117
+ updated_at timestamptz NOT NULL DEFAULT now(),
118
+ UNIQUE(pixel_id, name)
119
+ )
120
+ `),await t.query(`
121
+ CREATE TABLE IF NOT EXISTS analytics_host_group_members (
122
+ group_id uuid NOT NULL REFERENCES analytics_host_groups(id) ON DELETE CASCADE,
123
+ hostname text NOT NULL,
124
+ created_at timestamptz NOT NULL DEFAULT now(),
125
+ PRIMARY KEY(group_id, hostname)
126
+ )
127
+ `),await t.query(`
128
+ CREATE TABLE IF NOT EXISTS analytics_events (
129
+ id uuid PRIMARY KEY,
130
+ event_id text NOT NULL,
131
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
132
+ pixel_id uuid NOT NULL REFERENCES analytics_pixels(id) ON DELETE CASCADE,
133
+ hostname text NOT NULL,
134
+ visitor_id text,
135
+ session_id text,
136
+ visitor_hmac text,
137
+ session_hmac text,
138
+ event_name text NOT NULL,
139
+ occurred_at timestamptz NOT NULL,
140
+ received_at timestamptz NOT NULL DEFAULT now(),
141
+ path text NOT NULL DEFAULT '/',
142
+ canonical_url text,
143
+ title text,
144
+ referrer text,
145
+ source text,
146
+ medium text,
147
+ campaign text,
148
+ device_class text,
149
+ properties jsonb NOT NULL DEFAULT '{}'::jsonb,
150
+ bot_status text NOT NULL DEFAULT 'unknown',
151
+ UNIQUE(site_id, event_id)
152
+ )
153
+ `),await t.query(`
154
+ CREATE TABLE IF NOT EXISTS analytics_conversions (
155
+ id uuid PRIMARY KEY,
156
+ source_event_id text NOT NULL,
157
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
158
+ pixel_id uuid REFERENCES analytics_pixels(id) ON DELETE SET NULL,
159
+ session_id text,
160
+ session_hmac text,
161
+ conversion_kind text NOT NULL,
162
+ value_minor bigint NOT NULL DEFAULT 0,
163
+ currency text NOT NULL DEFAULT 'USD',
164
+ occurred_at timestamptz NOT NULL,
165
+ created_at timestamptz NOT NULL DEFAULT now(),
166
+ UNIQUE(site_id, source_event_id)
167
+ )
168
+ `),await t.query(`
169
+ CREATE TABLE IF NOT EXISTS analytics_ingestion_receipts (
170
+ id uuid PRIMARY KEY,
171
+ request_id text NOT NULL UNIQUE,
172
+ site_id uuid REFERENCES analytics_sites(id) ON DELETE SET NULL,
173
+ pixel_id uuid REFERENCES analytics_pixels(id) ON DELETE SET NULL,
174
+ hostname text,
175
+ accepted_count integer NOT NULL DEFAULT 0,
176
+ rejected_count integer NOT NULL DEFAULT 0,
177
+ reason_codes jsonb NOT NULL DEFAULT '[]'::jsonb,
178
+ created_at timestamptz NOT NULL DEFAULT now()
179
+ )
180
+ `),await t.query(`
181
+ CREATE TABLE IF NOT EXISTS analytics_rollups_daily (
182
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
183
+ pixel_id uuid NOT NULL REFERENCES analytics_pixels(id) ON DELETE CASCADE,
184
+ hostname text NOT NULL,
185
+ bucket date NOT NULL,
186
+ source text NOT NULL DEFAULT '(direct)',
187
+ medium text NOT NULL DEFAULT '(none)',
188
+ campaign text NOT NULL DEFAULT '(none)',
189
+ pageviews integer NOT NULL DEFAULT 0,
190
+ events integer NOT NULL DEFAULT 0,
191
+ visitors integer NOT NULL DEFAULT 0,
192
+ sessions integer NOT NULL DEFAULT 0,
193
+ refreshed_at timestamptz NOT NULL DEFAULT now(),
194
+ PRIMARY KEY(site_id, pixel_id, hostname, bucket, source, medium, campaign)
195
+ )
196
+ `),await t.query(`
197
+ CREATE TABLE IF NOT EXISTS analytics_rollup_runs (
198
+ id uuid PRIMARY KEY,
199
+ window_start timestamptz NOT NULL,
200
+ window_end timestamptz NOT NULL,
201
+ status text NOT NULL CHECK (status IN ('running', 'complete', 'failed')),
202
+ row_count integer NOT NULL DEFAULT 0,
203
+ started_at timestamptz NOT NULL DEFAULT now(),
204
+ completed_at timestamptz,
205
+ error_code text
206
+ )
207
+ `),await t.query(`
208
+ CREATE TABLE IF NOT EXISTS analytics_exports (
209
+ id uuid PRIMARY KEY,
210
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
211
+ requested_by_user_id bigint NOT NULL,
212
+ idempotency_key text,
213
+ report text NOT NULL,
214
+ format text NOT NULL CHECK (format IN ('csv', 'json', 'markdown')),
215
+ filters jsonb NOT NULL,
216
+ content text NOT NULL,
217
+ created_at timestamptz NOT NULL DEFAULT now()
218
+ )
219
+ `),await t.query(`
220
+ CREATE TABLE IF NOT EXISTS analytics_campaign_links (
221
+ id uuid PRIMARY KEY,
222
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
223
+ pixel_id uuid REFERENCES analytics_pixels(id) ON DELETE SET NULL,
224
+ name text NOT NULL,
225
+ destination_url text NOT NULL,
226
+ short_code text NOT NULL UNIQUE,
227
+ utm_source text NOT NULL,
228
+ utm_medium text NOT NULL,
229
+ utm_campaign text NOT NULL,
230
+ utm_term text,
231
+ utm_content text,
232
+ ad_group text,
233
+ ad_name text,
234
+ creative_id text,
235
+ status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'archived')),
236
+ created_by_user_id bigint NOT NULL,
237
+ created_at timestamptz NOT NULL DEFAULT now(),
238
+ updated_at timestamptz NOT NULL DEFAULT now()
239
+ )
240
+ `),await t.query(`
241
+ CREATE TABLE IF NOT EXISTS analytics_campaign_clicks (
242
+ id uuid PRIMARY KEY,
243
+ link_id uuid NOT NULL REFERENCES analytics_campaign_links(id) ON DELETE CASCADE,
244
+ occurred_at timestamptz NOT NULL DEFAULT now(),
245
+ referrer text
246
+ )
247
+ `),await t.query(`
248
+ CREATE TABLE IF NOT EXISTS analytics_forms (
249
+ id uuid PRIMARY KEY,
250
+ public_id text NOT NULL UNIQUE,
251
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
252
+ pixel_id uuid NOT NULL REFERENCES analytics_pixels(id) ON DELETE RESTRICT,
253
+ name text NOT NULL,
254
+ slug text NOT NULL,
255
+ fields jsonb NOT NULL,
256
+ brand jsonb NOT NULL,
257
+ submit_label text NOT NULL DEFAULT 'Submit',
258
+ success_message text NOT NULL DEFAULT 'Thank you. Your response was received.',
259
+ redirect_url text,
260
+ consent_text text,
261
+ status text NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'published', 'archived')),
262
+ version integer NOT NULL DEFAULT 1,
263
+ created_by_user_id bigint NOT NULL,
264
+ created_at timestamptz NOT NULL DEFAULT now(),
265
+ updated_at timestamptz NOT NULL DEFAULT now(),
266
+ UNIQUE(site_id, slug)
267
+ )
268
+ `),await t.query(`
269
+ CREATE TABLE IF NOT EXISTS analytics_form_submissions (
270
+ id uuid PRIMARY KEY,
271
+ form_id uuid NOT NULL REFERENCES analytics_forms(id) ON DELETE CASCADE,
272
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
273
+ pixel_id uuid NOT NULL REFERENCES analytics_pixels(id) ON DELETE RESTRICT,
274
+ visitor_id text,
275
+ session_id text,
276
+ crm_person_ref text,
277
+ source text,
278
+ medium text,
279
+ campaign text,
280
+ occurred_at timestamptz NOT NULL DEFAULT now()
281
+ )
282
+ `),await t.query("ALTER TABLE analytics_form_submissions ADD COLUMN IF NOT EXISTS crm_delivery_status text NOT NULL DEFAULT 'pending'"),await t.query("ALTER TABLE analytics_form_submissions ADD COLUMN IF NOT EXISTS crm_entity_id uuid"),await t.query("ALTER TABLE analytics_form_submissions ADD COLUMN IF NOT EXISTS bridge_run_id uuid"),await t.query("ALTER TABLE analytics_form_submissions ADD COLUMN IF NOT EXISTS idempotency_key text"),await t.query(`CREATE UNIQUE INDEX IF NOT EXISTS analytics_form_submissions_idempotency
283
+ ON analytics_form_submissions(form_id, idempotency_key)
284
+ WHERE idempotency_key IS NOT NULL`),await t.query(`
285
+ CREATE TABLE IF NOT EXISTS analytics_form_delivery_jobs (
286
+ id uuid PRIMARY KEY,
287
+ submission_id uuid NOT NULL UNIQUE REFERENCES analytics_form_submissions(id) ON DELETE CASCADE,
288
+ owner_user_id bigint NOT NULL,
289
+ payload_ciphertext text NOT NULL,
290
+ status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'delivered', 'failed', 'expired')),
291
+ attempts integer NOT NULL DEFAULT 0,
292
+ last_error_code text,
293
+ next_attempt_at timestamptz NOT NULL DEFAULT now(),
294
+ expires_at timestamptz NOT NULL DEFAULT now() + interval '7 days',
295
+ created_at timestamptz NOT NULL DEFAULT now(),
296
+ delivered_at timestamptz
297
+ )
298
+ `),await t.query("ALTER TABLE analytics_form_delivery_jobs ADD COLUMN IF NOT EXISTS bridge_run_id uuid"),await t.query("ALTER TABLE analytics_form_delivery_jobs ADD COLUMN IF NOT EXISTS crm_entity_id uuid"),await t.query("ALTER TABLE analytics_form_delivery_jobs ADD COLUMN IF NOT EXISTS idempotency_key text"),await t.query(`
299
+ CREATE TABLE IF NOT EXISTS analytics_form_bridge_receipts (
300
+ id uuid PRIMARY KEY,
301
+ bridge_run_id uuid NOT NULL,
302
+ submission_id uuid NOT NULL REFERENCES analytics_form_submissions(id) ON DELETE CASCADE,
303
+ system text NOT NULL CHECK (system IN ('analytics','crm')),
304
+ status text NOT NULL CHECK (status IN ('pending','delivered','failed','expired')),
305
+ opaque_entity_id uuid,
306
+ safe_error_code text,
307
+ occurred_at timestamptz NOT NULL DEFAULT now(),
308
+ updated_at timestamptz NOT NULL DEFAULT now(),
309
+ UNIQUE(bridge_run_id, system)
310
+ )
311
+ `),await t.query(`
312
+ CREATE TABLE IF NOT EXISTS analytics_people (
313
+ id uuid PRIMARY KEY,
314
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
315
+ crm_person_ref text NOT NULL,
316
+ first_seen_at timestamptz NOT NULL DEFAULT now(),
317
+ last_seen_at timestamptz NOT NULL DEFAULT now(),
318
+ UNIQUE(site_id, crm_person_ref)
319
+ )
320
+ `),await t.query("ALTER TABLE analytics_people ADD COLUMN IF NOT EXISTS crm_entity_id uuid"),await t.query(`CREATE UNIQUE INDEX IF NOT EXISTS analytics_people_crm_entity
321
+ ON analytics_people(site_id, crm_entity_id)
322
+ WHERE crm_entity_id IS NOT NULL`),await t.query(`
323
+ CREATE TABLE IF NOT EXISTS analytics_identity_nodes (
324
+ id uuid PRIMARY KEY,
325
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
326
+ kind text NOT NULL CHECK (kind IN ('visitor_id','session_id','email','phone','customer_id','click_id','device_signal','network_signal')),
327
+ value_hmac text NOT NULL,
328
+ first_seen_at timestamptz NOT NULL DEFAULT now(),
329
+ last_seen_at timestamptz NOT NULL DEFAULT now(),
330
+ UNIQUE(site_id, kind, value_hmac)
331
+ )
332
+ `),await t.query(`
333
+ CREATE TABLE IF NOT EXISTS analytics_identity_edges (
334
+ id uuid PRIMARY KEY,
335
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
336
+ person_id uuid NOT NULL REFERENCES analytics_people(id) ON DELETE CASCADE,
337
+ identity_node_id uuid NOT NULL REFERENCES analytics_identity_nodes(id) ON DELETE CASCADE,
338
+ evidence_kind text NOT NULL,
339
+ confidence numeric(4,3) NOT NULL CHECK (confidence >= 0 AND confidence <= 1),
340
+ consent_scope text NOT NULL DEFAULT 'analytics',
341
+ first_seen_at timestamptz NOT NULL DEFAULT now(),
342
+ last_seen_at timestamptz NOT NULL DEFAULT now(),
343
+ UNIQUE(person_id, identity_node_id, evidence_kind)
344
+ )
345
+ `),await t.query(`
346
+ CREATE TABLE IF NOT EXISTS analytics_identity_assignments (
347
+ identity_node_id uuid PRIMARY KEY REFERENCES analytics_identity_nodes(id) ON DELETE CASCADE,
348
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
349
+ person_id uuid NOT NULL REFERENCES analytics_people(id) ON DELETE CASCADE,
350
+ crm_entity_id uuid,
351
+ evidence_kind text NOT NULL,
352
+ assigned_at timestamptz NOT NULL DEFAULT now(),
353
+ updated_at timestamptz NOT NULL DEFAULT now(),
354
+ CHECK (crm_entity_id IS NULL OR crm_entity_id::text <> '')
355
+ )
356
+ `),await t.query(`
357
+ CREATE TABLE IF NOT EXISTS analytics_identity_assignment_conflicts (
358
+ id uuid PRIMARY KEY,
359
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
360
+ identity_node_id uuid REFERENCES analytics_identity_nodes(id) ON DELETE CASCADE,
361
+ active_person_id uuid REFERENCES analytics_people(id) ON DELETE CASCADE,
362
+ attempted_person_id uuid REFERENCES analytics_people(id) ON DELETE CASCADE,
363
+ active_crm_entity_id uuid,
364
+ attempted_crm_entity_id uuid,
365
+ evidence_kind text NOT NULL,
366
+ status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','resolved','dismissed')),
367
+ observed_at timestamptz NOT NULL DEFAULT now(),
368
+ resolved_at timestamptz,
369
+ CONSTRAINT analytics_identity_assignment_conflicts_pending_identity
370
+ CHECK (status <> 'pending' OR (
371
+ identity_node_id IS NOT NULL AND active_person_id IS NOT NULL
372
+ AND attempted_person_id IS NOT NULL AND active_person_id <> attempted_person_id
373
+ ))
374
+ )
375
+ `),await t.query("ALTER TABLE analytics_identity_assignment_conflicts ALTER COLUMN identity_node_id DROP NOT NULL"),await t.query("ALTER TABLE analytics_identity_assignment_conflicts ALTER COLUMN active_person_id DROP NOT NULL"),await t.query("ALTER TABLE analytics_identity_assignment_conflicts ALTER COLUMN attempted_person_id DROP NOT NULL"),await t.query(`ALTER TABLE analytics_identity_assignment_conflicts
376
+ DROP CONSTRAINT IF EXISTS analytics_identity_assignment_conflicts_check`),await t.query(`
377
+ DO $$ BEGIN
378
+ IF NOT EXISTS (
379
+ SELECT 1 FROM pg_constraint
380
+ WHERE conrelid='analytics_identity_assignment_conflicts'::regclass
381
+ AND conname='analytics_identity_assignment_conflicts_pending_identity'
382
+ ) THEN
383
+ ALTER TABLE analytics_identity_assignment_conflicts
384
+ ADD CONSTRAINT analytics_identity_assignment_conflicts_pending_identity
385
+ CHECK (status <> 'pending' OR (
386
+ identity_node_id IS NOT NULL AND active_person_id IS NOT NULL
387
+ AND attempted_person_id IS NOT NULL AND active_person_id <> attempted_person_id
388
+ ));
389
+ END IF;
390
+ END $$
391
+ `),await t.query(`CREATE UNIQUE INDEX IF NOT EXISTS analytics_identity_assignment_conflicts_pending
392
+ ON analytics_identity_assignment_conflicts(identity_node_id, active_person_id, attempted_person_id)
393
+ WHERE status='pending'`),await t.query(`
394
+ CREATE TABLE IF NOT EXISTS analytics_linker_tokens (
395
+ id uuid PRIMARY KEY,
396
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
397
+ pixel_id uuid NOT NULL REFERENCES analytics_pixels(id) ON DELETE CASCADE,
398
+ host_group_id uuid NOT NULL REFERENCES analytics_host_groups(id) ON DELETE CASCADE,
399
+ identity_node_id uuid NOT NULL REFERENCES analytics_identity_nodes(id) ON DELETE CASCADE,
400
+ source_hostname text NOT NULL,
401
+ audience_hostname text NOT NULL,
402
+ expires_at timestamptz NOT NULL,
403
+ consumed_at timestamptz,
404
+ created_at timestamptz NOT NULL DEFAULT now()
405
+ )
406
+ `),await t.query("ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS click_ids jsonb NOT NULL DEFAULT '{}'::jsonb"),await t.query("ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS visitor_hmac text"),await t.query("ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS session_hmac text"),await t.query("ALTER TABLE analytics_conversions ADD COLUMN IF NOT EXISTS session_hmac text"),await Ti(t),await t.query("ALTER TABLE analytics_form_submissions ADD COLUMN IF NOT EXISTS click_ids jsonb NOT NULL DEFAULT '{}'::jsonb"),await t.query("ALTER TABLE analytics_form_submissions ADD COLUMN IF NOT EXISTS person_id uuid REFERENCES analytics_people(id) ON DELETE SET NULL"),await t.query("ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS person_id uuid REFERENCES analytics_people(id) ON DELETE SET NULL"),await t.query("ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS channel_family text"),await t.query("ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS platform text"),await t.query("ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS campaign_id text"),await t.query("ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS ad_set_id text"),await t.query("ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS ad_id text"),await t.query("ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS creative_id text"),await t.query("ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS placement text"),await t.query("ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS utm_term text"),await t.query("ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS utm_content text"),await t.query("ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS engaged_ms integer"),await t.query("ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS scroll_depth smallint"),await t.query("ALTER TABLE analytics_conversions ADD COLUMN IF NOT EXISTS person_id uuid REFERENCES analytics_people(id) ON DELETE SET NULL"),await t.query("ALTER TABLE analytics_sites ADD COLUMN IF NOT EXISTS business_model text NOT NULL DEFAULT 'leadgen' CHECK (business_model IN ('leadgen','saas','ecommerce'))"),await t.query(`
407
+ CREATE TABLE IF NOT EXISTS analytics_ad_spend_daily (
408
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
409
+ bucket date NOT NULL,
410
+ platform text NOT NULL,
411
+ campaign text NOT NULL DEFAULT '(all)',
412
+ spend_minor bigint NOT NULL CHECK (spend_minor >= 0),
413
+ currency text NOT NULL DEFAULT 'USD',
414
+ source_ref text,
415
+ updated_at timestamptz NOT NULL DEFAULT now(),
416
+ PRIMARY KEY(site_id, bucket, platform, campaign)
417
+ )
418
+ `),await t.query(`
419
+ CREATE TABLE IF NOT EXISTS analytics_crm_imports (
420
+ id uuid PRIMARY KEY,
421
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
422
+ requested_by_user_id bigint NOT NULL,
423
+ source_system text NOT NULL,
424
+ filename text NOT NULL,
425
+ status text NOT NULL DEFAULT 'queued' CHECK (status IN ('queued','processing','complete','partial','failed')),
426
+ accepted_count integer NOT NULL DEFAULT 0,
427
+ rejected_count integer NOT NULL DEFAULT 0,
428
+ created_at timestamptz NOT NULL DEFAULT now(),
429
+ completed_at timestamptz
430
+ )
431
+ `),await t.query(`
432
+ CREATE TABLE IF NOT EXISTS analytics_crm_import_rows (
433
+ id uuid PRIMARY KEY,
434
+ import_id uuid NOT NULL REFERENCES analytics_crm_imports(id) ON DELETE CASCADE,
435
+ crm_person_ref text NOT NULL,
436
+ payload_ciphertext text NOT NULL,
437
+ delivery_status text NOT NULL DEFAULT 'pending' CHECK (delivery_status IN ('pending','delivered','failed','expired')),
438
+ attempts integer NOT NULL DEFAULT 0,
439
+ last_error_code text,
440
+ created_at timestamptz NOT NULL DEFAULT now(),
441
+ delivered_at timestamptz,
442
+ UNIQUE(import_id, crm_person_ref)
443
+ )
444
+ `),await t.query(`
445
+ CREATE TABLE IF NOT EXISTS analytics_activation_destinations (
446
+ id uuid PRIMARY KEY,
447
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
448
+ platform text NOT NULL CHECK (platform IN ('meta','google','tiktok','reddit')),
449
+ name text NOT NULL,
450
+ connection_ref text,
451
+ external_dataset_id text,
452
+ status text NOT NULL DEFAULT 'active' CHECK (status IN ('active','paused','archived')),
453
+ automatic_delivery_enabled boolean NOT NULL DEFAULT false,
454
+ event_mapping jsonb NOT NULL DEFAULT '{}'::jsonb,
455
+ created_by_user_id bigint NOT NULL,
456
+ created_at timestamptz NOT NULL DEFAULT now(),
457
+ updated_at timestamptz NOT NULL DEFAULT now()
458
+ )
459
+ `),await t.query(`
460
+ CREATE TABLE IF NOT EXISTS analytics_activation_jobs (
461
+ id uuid PRIMARY KEY,
462
+ destination_id uuid NOT NULL REFERENCES analytics_activation_destinations(id) ON DELETE CASCADE,
463
+ conversion_id uuid NOT NULL REFERENCES analytics_conversions(id) ON DELETE CASCADE,
464
+ person_id uuid REFERENCES analytics_people(id) ON DELETE SET NULL,
465
+ payload_ciphertext text NOT NULL,
466
+ status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','delivered','failed','expired')),
467
+ attempts integer NOT NULL DEFAULT 0,
468
+ last_error_code text,
469
+ next_attempt_at timestamptz NOT NULL DEFAULT now(),
470
+ created_at timestamptz NOT NULL DEFAULT now(),
471
+ delivered_at timestamptz,
472
+ UNIQUE(destination_id, conversion_id)
473
+ )
474
+ `),await t.query(`
475
+ CREATE TABLE IF NOT EXISTS analytics_thorbit_entitlements (
476
+ user_id bigint PRIMARY KEY,
477
+ thorbit_org_id text,
478
+ connection_source text NOT NULL DEFAULT 'trial' CHECK (connection_source IN ('trial','api_key','thorbit_session')),
479
+ eligible boolean NOT NULL DEFAULT false,
480
+ reason text NOT NULL,
481
+ plan_id text,
482
+ subscription_status text,
483
+ monthly_recurring_revenue_cents integer NOT NULL DEFAULT 0,
484
+ minimum_monthly_recurring_revenue_cents integer NOT NULL DEFAULT 5000,
485
+ verified_at timestamptz NOT NULL,
486
+ refresh_after timestamptz NOT NULL,
487
+ access_expires_at timestamptz NOT NULL,
488
+ trial_started_at timestamptz NOT NULL DEFAULT now(),
489
+ trial_ends_at timestamptz NOT NULL DEFAULT (now() + interval '30 days'),
490
+ last_refresh_error text,
491
+ updated_at timestamptz NOT NULL DEFAULT now()
492
+ )
493
+ `),await t.query("ALTER TABLE analytics_thorbit_entitlements ALTER COLUMN thorbit_org_id DROP NOT NULL"),await t.query("ALTER TABLE analytics_thorbit_entitlements ADD COLUMN IF NOT EXISTS trial_started_at timestamptz NOT NULL DEFAULT now()"),await t.query("ALTER TABLE analytics_thorbit_entitlements ADD COLUMN IF NOT EXISTS trial_ends_at timestamptz NOT NULL DEFAULT (now() + interval '30 days')"),await t.query("ALTER TABLE analytics_thorbit_entitlements DROP CONSTRAINT IF EXISTS analytics_thorbit_entitlements_connection_source_check"),await t.query("ALTER TABLE analytics_thorbit_entitlements ADD CONSTRAINT analytics_thorbit_entitlements_connection_source_check CHECK (connection_source IN ('trial','api_key','thorbit_session'))"),await t.query(`
494
+ CREATE TABLE IF NOT EXISTS analytics_thorbit_connect_states (
495
+ state_hash text PRIMARY KEY,
496
+ user_id bigint NOT NULL,
497
+ expires_at timestamptz NOT NULL,
498
+ consumed_at timestamptz,
499
+ created_at timestamptz NOT NULL DEFAULT now()
500
+ )
501
+ `),await t.query("ALTER TABLE analytics_exports ADD COLUMN IF NOT EXISTS idempotency_key text"),await t.query("ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS country_code text"),await t.query("ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS region_code text"),await t.query(`
502
+ CREATE TABLE IF NOT EXISTS analytics_sessions (
503
+ id uuid PRIMARY KEY,
504
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
505
+ pixel_id uuid REFERENCES analytics_pixels(id) ON DELETE SET NULL,
506
+ visitor_hmac text NOT NULL,
507
+ session_hmac text NOT NULL,
508
+ person_id uuid REFERENCES analytics_people(id) ON DELETE SET NULL,
509
+ started_at timestamptz NOT NULL,
510
+ last_active_at timestamptz NOT NULL,
511
+ ended_at timestamptz,
512
+ duration_ms bigint NOT NULL DEFAULT 0 CHECK (duration_ms >= 0),
513
+ landing_path text,
514
+ exit_path text,
515
+ pageview_count integer NOT NULL DEFAULT 0,
516
+ event_count integer NOT NULL DEFAULT 0,
517
+ first_touch_id uuid,
518
+ last_touch_id uuid,
519
+ created_at timestamptz NOT NULL DEFAULT now(),
520
+ updated_at timestamptz NOT NULL DEFAULT now(),
521
+ UNIQUE(site_id, session_hmac)
522
+ )
523
+ `),await t.query(`
524
+ CREATE TABLE IF NOT EXISTS analytics_attribution_touches (
525
+ id uuid PRIMARY KEY,
526
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
527
+ session_id uuid REFERENCES analytics_sessions(id) ON DELETE SET NULL,
528
+ person_id uuid REFERENCES analytics_people(id) ON DELETE SET NULL,
529
+ visitor_hmac text,
530
+ occurred_at timestamptz NOT NULL,
531
+ source text,
532
+ medium text,
533
+ campaign text,
534
+ referrer text,
535
+ click_ids jsonb NOT NULL DEFAULT '{}'::jsonb,
536
+ landing_path text,
537
+ touch_hash text NOT NULL,
538
+ created_at timestamptz NOT NULL DEFAULT now(),
539
+ UNIQUE(site_id, touch_hash)
540
+ )
541
+ `),await t.query(`
542
+ CREATE TABLE IF NOT EXISTS analytics_connections (
543
+ id uuid PRIMARY KEY,
544
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
545
+ provider text NOT NULL,
546
+ name text NOT NULL,
547
+ source_account_ref text NOT NULL,
548
+ service_connection_ref text,
549
+ secret_ciphertext text,
550
+ config jsonb NOT NULL DEFAULT '{}'::jsonb,
551
+ readiness text NOT NULL DEFAULT 'configured_unverified'
552
+ CHECK (readiness IN ('not_configured','configured_unverified','verified','live','degraded','disabled')),
553
+ last_verified_at timestamptz,
554
+ last_event_at timestamptz,
555
+ last_reconciled_at timestamptz,
556
+ last_error_code text,
557
+ verification_evidence text CHECK (verification_evidence IN ('synthetic','provider')),
558
+ last_verified_receipt_id uuid,
559
+ created_by_user_id bigint NOT NULL,
560
+ created_at timestamptz NOT NULL DEFAULT now(),
561
+ updated_at timestamptz NOT NULL DEFAULT now(),
562
+ UNIQUE(site_id, provider, source_account_ref)
563
+ )
564
+ `),await t.query(`
565
+ CREATE TABLE IF NOT EXISTS analytics_webhook_receipts (
566
+ id uuid PRIMARY KEY,
567
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
568
+ connection_id uuid REFERENCES analytics_connections(id) ON DELETE SET NULL,
569
+ provider text NOT NULL,
570
+ source_account_ref text NOT NULL,
571
+ source_event_id text NOT NULL,
572
+ signature_status text NOT NULL CHECK (signature_status IN ('verified','invalid','not_supported')),
573
+ normalization_status text NOT NULL DEFAULT 'pending'
574
+ CHECK (normalization_status IN ('pending','accepted','rejected','replayed')),
575
+ payload_ciphertext text,
576
+ payload_expires_at timestamptz,
577
+ error_code text,
578
+ received_at timestamptz NOT NULL DEFAULT now(),
579
+ normalized_at timestamptz,
580
+ UNIQUE(site_id, provider, source_account_ref, source_event_id)
581
+ )
582
+ `),await t.query(`
583
+ CREATE TABLE IF NOT EXISTS analytics_external_events (
584
+ id uuid PRIMARY KEY,
585
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
586
+ receipt_id uuid REFERENCES analytics_webhook_receipts(id) ON DELETE SET NULL,
587
+ person_id uuid REFERENCES analytics_people(id) ON DELETE SET NULL,
588
+ source text NOT NULL,
589
+ source_account_ref text NOT NULL,
590
+ source_event_id text NOT NULL,
591
+ event_kind text NOT NULL CHECK (event_kind IN ('form','call','crm','transaction','custom_server','conversion','delivery')),
592
+ event_name text NOT NULL,
593
+ occurred_at timestamptz NOT NULL,
594
+ visitor_hmac text,
595
+ session_hmac text,
596
+ call_id text,
597
+ deal_id text,
598
+ order_id text,
599
+ value_minor bigint,
600
+ currency text,
601
+ click_ids jsonb NOT NULL DEFAULT '{}'::jsonb,
602
+ properties jsonb NOT NULL DEFAULT '{}'::jsonb,
603
+ created_at timestamptz NOT NULL DEFAULT now(),
604
+ UNIQUE(site_id, source, source_account_ref, source_event_id)
605
+ )
606
+ `),await t.query(`
607
+ CREATE TABLE IF NOT EXISTS analytics_reconciliation_cursors (
608
+ connection_id uuid PRIMARY KEY REFERENCES analytics_connections(id) ON DELETE CASCADE,
609
+ cursor jsonb,
610
+ health text NOT NULL DEFAULT 'webhook_only'
611
+ CHECK (health IN ('webhook_only','reconciled','degraded')),
612
+ records_replayed bigint NOT NULL DEFAULT 0,
613
+ last_started_at timestamptz,
614
+ last_completed_at timestamptz,
615
+ last_source_event_id text,
616
+ last_error_code text,
617
+ updated_at timestamptz NOT NULL DEFAULT now()
618
+ )
619
+ `),await t.query(`
620
+ CREATE TABLE IF NOT EXISTS analytics_reconciliation_jobs (
621
+ id uuid PRIMARY KEY,
622
+ connection_id uuid NOT NULL UNIQUE REFERENCES analytics_connections(id) ON DELETE CASCADE,
623
+ status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','leased')),
624
+ attempts integer NOT NULL DEFAULT 0,
625
+ next_attempt_at timestamptz NOT NULL DEFAULT now(),
626
+ lease_until timestamptz,
627
+ last_error_code text,
628
+ created_at timestamptz NOT NULL DEFAULT now(),
629
+ updated_at timestamptz NOT NULL DEFAULT now()
630
+ )
631
+ `),await t.query(`
632
+ CREATE TABLE IF NOT EXISTS analytics_event_definitions (
633
+ id uuid PRIMARY KEY,
634
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
635
+ name text NOT NULL,
636
+ event_name text NOT NULL,
637
+ trigger_kind text NOT NULL CHECK (trigger_kind IN ('page','click','form','semantic','custom_server','call','crm_stage','transaction')),
638
+ definition jsonb NOT NULL,
639
+ version integer NOT NULL DEFAULT 1,
640
+ status text NOT NULL DEFAULT 'active' CHECK (status IN ('active','disabled','archived')),
641
+ created_by_user_id bigint NOT NULL,
642
+ created_at timestamptz NOT NULL DEFAULT now(),
643
+ updated_at timestamptz NOT NULL DEFAULT now(),
644
+ UNIQUE(site_id, name)
645
+ )
646
+ `),await t.query(`
647
+ DO $$
648
+ BEGIN
649
+ IF EXISTS (
650
+ SELECT 1
651
+ FROM pg_constraint
652
+ WHERE conrelid = 'analytics_event_definitions'::regclass
653
+ AND conname = 'analytics_event_definitions_trigger_kind_check'
654
+ AND pg_get_constraintdef(oid) NOT LIKE '%semantic%'
655
+ ) THEN
656
+ ALTER TABLE analytics_event_definitions
657
+ DROP CONSTRAINT analytics_event_definitions_trigger_kind_check;
658
+ ALTER TABLE analytics_event_definitions
659
+ ADD CONSTRAINT analytics_event_definitions_trigger_kind_check
660
+ CHECK (trigger_kind IN ('page','click','form','semantic','custom_server','call','crm_stage','transaction'));
661
+ END IF;
662
+ END $$
663
+ `),await t.query(`
664
+ CREATE TABLE IF NOT EXISTS analytics_conversion_rules (
665
+ id uuid PRIMARY KEY,
666
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
667
+ name text NOT NULL,
668
+ conversion_kind text NOT NULL,
669
+ condition jsonb NOT NULL,
670
+ default_value_minor bigint,
671
+ default_currency text NOT NULL DEFAULT 'USD',
672
+ version integer NOT NULL DEFAULT 1,
673
+ status text NOT NULL DEFAULT 'active' CHECK (status IN ('active','disabled','archived')),
674
+ created_by_user_id bigint NOT NULL,
675
+ created_at timestamptz NOT NULL DEFAULT now(),
676
+ updated_at timestamptz NOT NULL DEFAULT now(),
677
+ UNIQUE(site_id, name)
678
+ )
679
+ `),await t.query(`
680
+ CREATE TABLE IF NOT EXISTS analytics_restricted_data (
681
+ id uuid PRIMARY KEY,
682
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
683
+ person_id uuid REFERENCES analytics_people(id) ON DELETE CASCADE,
684
+ purpose text NOT NULL,
685
+ subject_ref text NOT NULL,
686
+ payload_ciphertext text NOT NULL,
687
+ expires_at timestamptz NOT NULL,
688
+ created_at timestamptz NOT NULL DEFAULT now(),
689
+ UNIQUE(site_id, purpose, subject_ref)
690
+ )
691
+ `),await t.query(`
692
+ CREATE TABLE IF NOT EXISTS analytics_identity_tombstones (
693
+ id uuid PRIMARY KEY,
694
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
695
+ person_id uuid,
696
+ kind text NOT NULL,
697
+ value_hmac text NOT NULL,
698
+ reason text NOT NULL CHECK (reason IN ('subject_deletion')),
699
+ created_at timestamptz NOT NULL DEFAULT now(),
700
+ UNIQUE(site_id,kind,value_hmac)
701
+ )
702
+ `),await t.query(`
703
+ CREATE TABLE IF NOT EXISTS analytics_identity_profile_access_audits (
704
+ id uuid PRIMARY KEY,
705
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
706
+ person_id uuid,
707
+ actor_user_id bigint NOT NULL,
708
+ subject_ref_hmac text NOT NULL,
709
+ purpose text NOT NULL CHECK (purpose IN ('journey_identity_display')),
710
+ result text NOT NULL CHECK (result IN ('returned_masked','not_found','unavailable','rate_limited')),
711
+ created_at timestamptz NOT NULL DEFAULT now()
712
+ )
713
+ `),await t.query("CREATE INDEX IF NOT EXISTS analytics_identity_profile_access_rate ON analytics_identity_profile_access_audits(site_id,actor_user_id,created_at DESC)"),await t.query("ALTER TABLE analytics_activation_destinations ADD COLUMN IF NOT EXISTS readiness text NOT NULL DEFAULT 'configured_unverified' CHECK (readiness IN ('not_configured','configured_unverified','verified','live','degraded','disabled'))"),await t.query("ALTER TABLE analytics_activation_destinations ADD COLUMN IF NOT EXISTS last_tested_at timestamptz"),await t.query("ALTER TABLE analytics_activation_destinations ADD COLUMN IF NOT EXISTS last_receipt_id text"),await t.query("ALTER TABLE analytics_activation_destinations ADD COLUMN IF NOT EXISTS provider_config jsonb NOT NULL DEFAULT '{}'::jsonb"),await t.query("ALTER TABLE analytics_activation_destinations ADD COLUMN IF NOT EXISTS mapping_version integer NOT NULL DEFAULT 1"),await t.query("ALTER TABLE analytics_activation_destinations ADD COLUMN IF NOT EXISTS automatic_delivery_enabled boolean NOT NULL DEFAULT false"),await t.query("ALTER TABLE analytics_activation_jobs ADD COLUMN IF NOT EXISTS lease_until timestamptz"),await t.query("ALTER TABLE analytics_activation_jobs ADD COLUMN IF NOT EXISTS mapping_version integer NOT NULL DEFAULT 1"),await t.query("ALTER TABLE analytics_activation_jobs ADD COLUMN IF NOT EXISTS external_receipt_id text"),await t.query("ALTER TABLE analytics_activation_jobs ADD COLUMN IF NOT EXISTS response_code integer"),await t.query("ALTER TABLE analytics_activation_jobs ADD COLUMN IF NOT EXISTS response_category text"),await t.query("ALTER TABLE analytics_activation_jobs ADD COLUMN IF NOT EXISTS dead_lettered_at timestamptz"),await t.query(`
714
+ CREATE TABLE IF NOT EXISTS analytics_activation_destination_tests (
715
+ id uuid PRIMARY KEY,
716
+ destination_id uuid NOT NULL REFERENCES analytics_activation_destinations(id) ON DELETE CASCADE,
717
+ mapping_version integer NOT NULL,
718
+ provider_request_id text,
719
+ provider_event_id text,
720
+ warnings jsonb NOT NULL DEFAULT '[]'::jsonb,
721
+ safe_error_category text,
722
+ request_started_at timestamptz NOT NULL DEFAULT now(),
723
+ response_received_at timestamptz,
724
+ next_diagnostic_at timestamptz,
725
+ actor text NOT NULL,
726
+ accepted boolean NOT NULL DEFAULT false,
727
+ validation_only boolean NOT NULL DEFAULT false,
728
+ created_at timestamptz NOT NULL DEFAULT now()
729
+ )
730
+ `),await t.query(`
731
+ CREATE TABLE IF NOT EXISTS analytics_activation_delivery_attempts (
732
+ id uuid PRIMARY KEY,
733
+ job_id uuid NOT NULL REFERENCES analytics_activation_jobs(id) ON DELETE CASCADE,
734
+ destination_id uuid NOT NULL REFERENCES analytics_activation_destinations(id) ON DELETE CASCADE,
735
+ mapping_version integer NOT NULL,
736
+ attempt_number integer NOT NULL,
737
+ provider_request_id text,
738
+ provider_event_id text,
739
+ warnings jsonb NOT NULL DEFAULT '[]'::jsonb,
740
+ safe_error_category text,
741
+ request_started_at timestamptz NOT NULL DEFAULT now(),
742
+ response_received_at timestamptz,
743
+ next_diagnostic_at timestamptz,
744
+ actor text NOT NULL,
745
+ accepted boolean NOT NULL DEFAULT false,
746
+ created_at timestamptz NOT NULL DEFAULT now(),
747
+ UNIQUE(job_id, attempt_number)
748
+ )
749
+ `),await t.query("ALTER TABLE analytics_crm_import_rows ADD COLUMN IF NOT EXISTS next_attempt_at timestamptz NOT NULL DEFAULT now()"),await t.query("ALTER TABLE analytics_crm_import_rows ADD COLUMN IF NOT EXISTS lease_until timestamptz"),await t.query("ALTER TABLE analytics_crm_import_rows ADD COLUMN IF NOT EXISTS expires_at timestamptz NOT NULL DEFAULT (now() + interval '7 days')"),await t.query("ALTER TABLE analytics_conversions ADD COLUMN IF NOT EXISTS rule_id uuid REFERENCES analytics_conversion_rules(id) ON DELETE SET NULL"),await t.query("ALTER TABLE analytics_conversions ADD COLUMN IF NOT EXISTS rule_version integer"),await t.query("ALTER TABLE analytics_conversions ADD COLUMN IF NOT EXISTS order_id text"),await t.query("ALTER TABLE analytics_connections ADD COLUMN IF NOT EXISTS verification_evidence text CHECK (verification_evidence IN ('synthetic','provider'))"),await t.query("ALTER TABLE analytics_connections ADD COLUMN IF NOT EXISTS last_verified_receipt_id uuid"),await t.query("ALTER TABLE analytics_sessions ADD COLUMN IF NOT EXISTS public_ref text"),await t.query("ALTER TABLE analytics_people ADD COLUMN IF NOT EXISTS public_ref text"),await t.query(`UPDATE analytics_people
750
+ SET public_ref = 'xrp_' || substr(md5(random()::text || clock_timestamp()::text || id::text || site_id::text), 1, 24)
751
+ WHERE public_ref IS NULL`),await t.query("ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS event_definition_id uuid REFERENCES analytics_event_definitions(id) ON DELETE SET NULL"),await t.query("ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS event_definition_version integer"),await t.query("ALTER TABLE analytics_conversions ADD COLUMN IF NOT EXISTS value_source text NOT NULL DEFAULT 'default_zero' CHECK (value_source IN ('absent','default_zero','fixed','dynamic','conditional_override','provider','verified_revenue','restatement'))"),await t.query("ALTER TABLE analytics_conversions ADD COLUMN IF NOT EXISTS value_provenance jsonb NOT NULL DEFAULT '{}'::jsonb"),await t.query("ALTER TABLE analytics_attribution_touches ADD COLUMN IF NOT EXISTS touch_kind text NOT NULL DEFAULT 'click' CHECK (touch_kind IN ('click','view','direct','organic','referral','email','sms','call','other'))"),await t.query("ALTER TABLE analytics_attribution_touches ADD COLUMN IF NOT EXISTS platform text"),await t.query("ALTER TABLE analytics_attribution_touches ADD COLUMN IF NOT EXISTS account_id text"),await t.query("ALTER TABLE analytics_attribution_touches ADD COLUMN IF NOT EXISTS campaign_id text"),await t.query("ALTER TABLE analytics_attribution_touches ADD COLUMN IF NOT EXISTS ad_set_id text"),await t.query("ALTER TABLE analytics_attribution_touches ADD COLUMN IF NOT EXISTS ad_id text"),await t.query("ALTER TABLE analytics_attribution_touches ADD COLUMN IF NOT EXISTS creative_id text"),await t.query("ALTER TABLE analytics_attribution_touches ADD COLUMN IF NOT EXISTS placement text"),await t.query(`
752
+ CREATE TABLE IF NOT EXISTS analytics_onboarding_presentations (
753
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
754
+ user_id bigint NOT NULL,
755
+ preferences jsonb NOT NULL DEFAULT '{}'::jsonb,
756
+ last_presented_step text,
757
+ dismissed_help jsonb NOT NULL DEFAULT '[]'::jsonb,
758
+ created_at timestamptz NOT NULL DEFAULT now(),
759
+ updated_at timestamptz NOT NULL DEFAULT now(),
760
+ PRIMARY KEY(site_id, user_id)
761
+ )
762
+ `),await t.query(`
763
+ CREATE TABLE IF NOT EXISTS analytics_funnel_definitions (
764
+ id uuid PRIMARY KEY,
765
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
766
+ name text NOT NULL,
767
+ stages jsonb NOT NULL CHECK (jsonb_typeof(stages) = 'array'),
768
+ version integer NOT NULL DEFAULT 1 CHECK (version > 0),
769
+ status text NOT NULL DEFAULT 'active' CHECK (status IN ('active','disabled','archived')),
770
+ is_default boolean NOT NULL DEFAULT false,
771
+ created_by_user_id bigint NOT NULL,
772
+ created_at timestamptz NOT NULL DEFAULT now(),
773
+ updated_at timestamptz NOT NULL DEFAULT now(),
774
+ UNIQUE(site_id, name, version)
775
+ )
776
+ `),await t.query(`
777
+ CREATE TABLE IF NOT EXISTS analytics_saved_views (
778
+ id uuid PRIMARY KEY,
779
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
780
+ owner_user_id bigint NOT NULL,
781
+ name text NOT NULL,
782
+ report_type text NOT NULL,
783
+ schema_version integer NOT NULL DEFAULT 1 CHECK (schema_version > 0),
784
+ settings jsonb NOT NULL DEFAULT '{}'::jsonb,
785
+ is_default boolean NOT NULL DEFAULT false,
786
+ created_at timestamptz NOT NULL DEFAULT now(),
787
+ updated_at timestamptz NOT NULL DEFAULT now(),
788
+ UNIQUE(site_id, owner_user_id, report_type, name)
789
+ )
790
+ `),await t.query(`
791
+ CREATE TABLE IF NOT EXISTS analytics_product_milestones (
792
+ id uuid PRIMARY KEY,
793
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
794
+ user_id bigint NOT NULL,
795
+ milestone text NOT NULL,
796
+ evidence_kind text NOT NULL,
797
+ evidence_ref text,
798
+ occurred_at timestamptz NOT NULL DEFAULT now(),
799
+ safe_metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
800
+ UNIQUE(site_id, user_id, milestone)
801
+ )
802
+ `),await t.query(`
803
+ CREATE TABLE IF NOT EXISTS analytics_scheduled_occurrences (
804
+ id uuid PRIMARY KEY,
805
+ lane_key text NOT NULL,
806
+ scheduled_for timestamptz NOT NULL,
807
+ status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','running','retrying','succeeded','dead_letter')),
808
+ attempt_count integer NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
809
+ max_attempts integer NOT NULL DEFAULT 5 CHECK (max_attempts BETWEEN 1 AND 20),
810
+ next_attempt_at timestamptz NOT NULL,
811
+ lease_owner text,
812
+ lease_until timestamptz,
813
+ started_at timestamptz,
814
+ completed_at timestamptz,
815
+ dead_lettered_at timestamptz,
816
+ last_error_code text,
817
+ result_summary jsonb NOT NULL DEFAULT '{}'::jsonb,
818
+ created_at timestamptz NOT NULL DEFAULT now(),
819
+ updated_at timestamptz NOT NULL DEFAULT now(),
820
+ UNIQUE(lane_key, scheduled_for)
821
+ )
822
+ `),await t.query(`
823
+ CREATE TABLE IF NOT EXISTS analytics_purpose_policies (
824
+ id uuid PRIMARY KEY,
825
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
826
+ purpose text NOT NULL CHECK (purpose IN ('necessary','site_analytics','enhanced_matching','advertising_measurement')),
827
+ policy_version integer NOT NULL CHECK (policy_version > 0),
828
+ jurisdiction_profile text NOT NULL,
829
+ retention_days integer CHECK (retention_days IS NULL OR retention_days > 0),
830
+ enabled boolean NOT NULL DEFAULT true,
831
+ policy jsonb NOT NULL DEFAULT '{}'::jsonb,
832
+ created_by_user_id bigint NOT NULL,
833
+ created_at timestamptz NOT NULL DEFAULT now(),
834
+ retired_at timestamptz,
835
+ UNIQUE(site_id, purpose, policy_version, jurisdiction_profile)
836
+ )
837
+ `),await t.query(`
838
+ CREATE TABLE IF NOT EXISTS analytics_consent_receipts (
839
+ id uuid PRIMARY KEY,
840
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
841
+ pixel_id uuid REFERENCES analytics_pixels(id) ON DELETE SET NULL,
842
+ subject_ref text NOT NULL,
843
+ policy_version integer NOT NULL CHECK (policy_version > 0),
844
+ jurisdiction_profile text NOT NULL,
845
+ necessary boolean NOT NULL DEFAULT true CHECK (necessary),
846
+ site_analytics boolean NOT NULL DEFAULT false,
847
+ enhanced_matching boolean NOT NULL DEFAULT false,
848
+ advertising_measurement boolean NOT NULL DEFAULT false,
849
+ gpc boolean NOT NULL DEFAULT false,
850
+ action text NOT NULL CHECK (action IN ('accept_all','reject_all','save_choices','withdraw','gpc')),
851
+ source text NOT NULL CHECK (source IN ('gateway','api','gpc','system')),
852
+ presented_at timestamptz,
853
+ occurred_at timestamptz NOT NULL,
854
+ expires_at timestamptz,
855
+ supersedes_id uuid REFERENCES analytics_consent_receipts(id) ON DELETE SET NULL,
856
+ created_at timestamptz NOT NULL DEFAULT now()
857
+ )
858
+ `),await t.query("ALTER TABLE analytics_consent_receipts ADD COLUMN IF NOT EXISTS presented_at timestamptz"),await t.query(`
859
+ DO $$
860
+ BEGIN
861
+ IF NOT EXISTS (
862
+ SELECT 1 FROM pg_constraint
863
+ WHERE conrelid = 'analytics_consent_receipts'::regclass
864
+ AND conname = 'analytics_consent_receipts_presentation_order_check'
865
+ ) THEN
866
+ ALTER TABLE analytics_consent_receipts
867
+ ADD CONSTRAINT analytics_consent_receipts_presentation_order_check
868
+ CHECK (presented_at IS NULL OR presented_at <= occurred_at);
869
+ END IF;
870
+ END $$
871
+ `),await t.query(`
872
+ CREATE TABLE IF NOT EXISTS analytics_consent_current_state (
873
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
874
+ subject_ref text NOT NULL,
875
+ latest_receipt_id uuid NOT NULL REFERENCES analytics_consent_receipts(id) ON DELETE CASCADE,
876
+ policy_version integer NOT NULL CHECK (policy_version > 0),
877
+ jurisdiction_profile text NOT NULL,
878
+ necessary boolean NOT NULL DEFAULT true CHECK (necessary),
879
+ site_analytics boolean NOT NULL DEFAULT false,
880
+ enhanced_matching boolean NOT NULL DEFAULT false,
881
+ advertising_measurement boolean NOT NULL DEFAULT false,
882
+ gpc boolean NOT NULL DEFAULT false,
883
+ effective_at timestamptz NOT NULL,
884
+ expires_at timestamptz,
885
+ updated_at timestamptz NOT NULL DEFAULT now(),
886
+ PRIMARY KEY(site_id, subject_ref)
887
+ )
888
+ `),await t.query("ALTER TABLE analytics_restricted_data ADD COLUMN IF NOT EXISTS consent_receipt_id uuid REFERENCES analytics_consent_receipts(id) ON DELETE CASCADE"),await t.query(`
889
+ CREATE TABLE IF NOT EXISTS analytics_identity_namespaces (
890
+ id uuid PRIMARY KEY,
891
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
892
+ name text NOT NULL,
893
+ status text NOT NULL DEFAULT 'draft' CHECK (status IN ('draft','approved','disabled','archived')),
894
+ approved_hosts jsonb NOT NULL DEFAULT '[]'::jsonb CHECK (jsonb_typeof(approved_hosts) = 'array'),
895
+ approved_sources jsonb NOT NULL DEFAULT '[]'::jsonb CHECK (jsonb_typeof(approved_sources) = 'array'),
896
+ version integer NOT NULL DEFAULT 1 CHECK (version > 0),
897
+ approved_by_user_id bigint,
898
+ approved_at timestamptz,
899
+ created_at timestamptz NOT NULL DEFAULT now(),
900
+ updated_at timestamptz NOT NULL DEFAULT now(),
901
+ UNIQUE(site_id, name, version),
902
+ UNIQUE(id, site_id)
903
+ )
904
+ `),await t.query(`
905
+ CREATE TABLE IF NOT EXISTS analytics_identity_sources (
906
+ id uuid PRIMARY KEY,
907
+ namespace_id uuid NOT NULL,
908
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
909
+ source_kind text NOT NULL CHECK (source_kind IN ('pixel','host','form','crm','call','server','import')),
910
+ source_ref text NOT NULL,
911
+ status text NOT NULL DEFAULT 'approved' CHECK (status IN ('pending','approved','revoked')),
912
+ created_at timestamptz NOT NULL DEFAULT now(),
913
+ revoked_at timestamptz,
914
+ FOREIGN KEY(namespace_id, site_id) REFERENCES analytics_identity_namespaces(id, site_id) ON DELETE CASCADE,
915
+ UNIQUE(namespace_id, source_kind, source_ref)
916
+ )
917
+ `),await t.query("ALTER TABLE analytics_people ADD COLUMN IF NOT EXISTS namespace_id uuid"),await t.query("ALTER TABLE analytics_identity_nodes ADD COLUMN IF NOT EXISTS namespace_id uuid"),await t.query("ALTER TABLE analytics_identity_edges ADD COLUMN IF NOT EXISTS namespace_id uuid"),await t.query("CREATE UNIQUE INDEX IF NOT EXISTS analytics_people_site_ref ON analytics_people(id, site_id)"),await t.query("CREATE UNIQUE INDEX IF NOT EXISTS analytics_identity_nodes_site_ref ON analytics_identity_nodes(id, site_id)"),await t.query("CREATE UNIQUE INDEX IF NOT EXISTS analytics_identity_nodes_namespace_ref ON analytics_identity_nodes(id, site_id, namespace_id)"),await t.query("CREATE UNIQUE INDEX IF NOT EXISTS analytics_people_namespace_ref ON analytics_people(id, site_id, namespace_id)"),await t.query(`
918
+ DO $$
919
+ BEGIN
920
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname='analytics_people_namespace_site_fkey') THEN
921
+ ALTER TABLE analytics_people ADD CONSTRAINT analytics_people_namespace_site_fkey
922
+ FOREIGN KEY(namespace_id, site_id) REFERENCES analytics_identity_namespaces(id, site_id)
923
+ ON DELETE RESTRICT NOT VALID;
924
+ END IF;
925
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname='analytics_identity_nodes_namespace_site_fkey') THEN
926
+ ALTER TABLE analytics_identity_nodes ADD CONSTRAINT analytics_identity_nodes_namespace_site_fkey
927
+ FOREIGN KEY(namespace_id, site_id) REFERENCES analytics_identity_namespaces(id, site_id)
928
+ ON DELETE RESTRICT NOT VALID;
929
+ END IF;
930
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname='analytics_identity_edges_person_site_fkey') THEN
931
+ ALTER TABLE analytics_identity_edges ADD CONSTRAINT analytics_identity_edges_person_site_fkey
932
+ FOREIGN KEY(person_id, site_id) REFERENCES analytics_people(id, site_id)
933
+ ON DELETE CASCADE NOT VALID;
934
+ END IF;
935
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname='analytics_identity_edges_node_site_fkey') THEN
936
+ ALTER TABLE analytics_identity_edges ADD CONSTRAINT analytics_identity_edges_node_site_fkey
937
+ FOREIGN KEY(identity_node_id, site_id) REFERENCES analytics_identity_nodes(id, site_id)
938
+ ON DELETE CASCADE NOT VALID;
939
+ END IF;
940
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname='analytics_identity_edges_namespace_site_fkey') THEN
941
+ ALTER TABLE analytics_identity_edges ADD CONSTRAINT analytics_identity_edges_namespace_site_fkey
942
+ FOREIGN KEY(namespace_id, site_id) REFERENCES analytics_identity_namespaces(id, site_id)
943
+ ON DELETE RESTRICT NOT VALID;
944
+ END IF;
945
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname='analytics_identity_edges_person_namespace_fkey') THEN
946
+ ALTER TABLE analytics_identity_edges ADD CONSTRAINT analytics_identity_edges_person_namespace_fkey
947
+ FOREIGN KEY(person_id, site_id, namespace_id) REFERENCES analytics_people(id, site_id, namespace_id)
948
+ ON DELETE CASCADE NOT VALID;
949
+ END IF;
950
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname='analytics_identity_edges_node_namespace_fkey') THEN
951
+ ALTER TABLE analytics_identity_edges ADD CONSTRAINT analytics_identity_edges_node_namespace_fkey
952
+ FOREIGN KEY(identity_node_id, site_id, namespace_id) REFERENCES analytics_identity_nodes(id, site_id, namespace_id)
953
+ ON DELETE CASCADE NOT VALID;
954
+ END IF;
955
+ END $$
956
+ `);for(let i of[["analytics_people","analytics_people_namespace_site_fkey"],["analytics_identity_nodes","analytics_identity_nodes_namespace_site_fkey"],["analytics_identity_edges","analytics_identity_edges_person_site_fkey"],["analytics_identity_edges","analytics_identity_edges_node_site_fkey"],["analytics_identity_edges","analytics_identity_edges_namespace_site_fkey"],["analytics_identity_edges","analytics_identity_edges_person_namespace_fkey"],["analytics_identity_edges","analytics_identity_edges_node_namespace_fkey"]])await t.query(`ALTER TABLE ${i[0]} VALIDATE CONSTRAINT ${i[1]}`);await t.query("ALTER TABLE analytics_people DROP CONSTRAINT IF EXISTS analytics_people_namespace_id_fkey"),await t.query("ALTER TABLE analytics_identity_nodes DROP CONSTRAINT IF EXISTS analytics_identity_nodes_namespace_id_fkey"),await t.query("ALTER TABLE analytics_identity_edges DROP CONSTRAINT IF EXISTS analytics_identity_edges_namespace_id_fkey"),await t.query("ALTER TABLE analytics_identity_edges DROP CONSTRAINT IF EXISTS analytics_identity_edges_person_id_fkey"),await t.query("ALTER TABLE analytics_identity_edges DROP CONSTRAINT IF EXISTS analytics_identity_edges_identity_node_id_fkey"),await t.query(`
957
+ CREATE TABLE IF NOT EXISTS analytics_candidate_associations (
958
+ id uuid PRIMARY KEY,
959
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
960
+ namespace_id uuid NOT NULL,
961
+ left_identity_node_id uuid NOT NULL,
962
+ right_identity_node_id uuid NOT NULL,
963
+ confidence numeric(5,4) NOT NULL CHECK (confidence >= 0 AND confidence <= 1),
964
+ confidence_band text NOT NULL CHECK (confidence_band IN ('high','medium','low','ineligible')),
965
+ status text NOT NULL DEFAULT 'active' CHECK (status IN ('active','expired','rejected','promoted','deleted')),
966
+ consent_receipt_id uuid NOT NULL REFERENCES analytics_consent_receipts(id) ON DELETE RESTRICT,
967
+ model_version text NOT NULL,
968
+ first_observed_at timestamptz NOT NULL,
969
+ last_observed_at timestamptz NOT NULL,
970
+ expires_at timestamptz NOT NULL,
971
+ promoted_person_id uuid,
972
+ promoted_at timestamptz,
973
+ created_at timestamptz NOT NULL DEFAULT now(),
974
+ updated_at timestamptz NOT NULL DEFAULT now(),
975
+ CHECK (left_identity_node_id <> right_identity_node_id),
976
+ FOREIGN KEY(namespace_id, site_id) REFERENCES analytics_identity_namespaces(id, site_id) ON DELETE CASCADE,
977
+ FOREIGN KEY(left_identity_node_id, site_id, namespace_id) REFERENCES analytics_identity_nodes(id, site_id, namespace_id) ON DELETE CASCADE,
978
+ FOREIGN KEY(right_identity_node_id, site_id, namespace_id) REFERENCES analytics_identity_nodes(id, site_id, namespace_id) ON DELETE CASCADE,
979
+ FOREIGN KEY(promoted_person_id, site_id, namespace_id) REFERENCES analytics_people(id, site_id, namespace_id) ON DELETE RESTRICT,
980
+ UNIQUE(namespace_id, left_identity_node_id, right_identity_node_id, model_version),
981
+ UNIQUE(id, namespace_id)
982
+ )
983
+ `),await t.query(`
984
+ CREATE TABLE IF NOT EXISTS analytics_candidate_evidence (
985
+ id uuid PRIMARY KEY,
986
+ association_id uuid NOT NULL,
987
+ namespace_id uuid NOT NULL,
988
+ feature_kind text NOT NULL CHECK (feature_kind IN ('device_bucket','browser_bucket','coarse_geo','network_bucket','temporal_pattern','journey_pattern')),
989
+ feature_hmac text NOT NULL,
990
+ contribution numeric(6,5) NOT NULL CHECK (contribution >= -1 AND contribution <= 1),
991
+ observed_at timestamptz NOT NULL,
992
+ expires_at timestamptz NOT NULL,
993
+ created_at timestamptz NOT NULL DEFAULT now(),
994
+ FOREIGN KEY(association_id, namespace_id) REFERENCES analytics_candidate_associations(id, namespace_id) ON DELETE CASCADE,
995
+ UNIQUE(association_id, feature_kind, feature_hmac)
996
+ )
997
+ `),await t.query(`
998
+ CREATE TABLE IF NOT EXISTS analytics_candidate_promotions (
999
+ id uuid PRIMARY KEY,
1000
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1001
+ namespace_id uuid NOT NULL,
1002
+ association_id uuid NOT NULL,
1003
+ person_id uuid NOT NULL,
1004
+ evidence_kind text NOT NULL CHECK (evidence_kind IN ('verified_email','verified_phone','crm_contact','signed_linker','authenticated_user')),
1005
+ evidence_ref_hmac text NOT NULL,
1006
+ promoted_by text NOT NULL CHECK (promoted_by IN ('system','user')),
1007
+ promoted_by_user_id bigint,
1008
+ promoted_at timestamptz NOT NULL DEFAULT now(),
1009
+ FOREIGN KEY(namespace_id, site_id) REFERENCES analytics_identity_namespaces(id, site_id) ON DELETE CASCADE,
1010
+ FOREIGN KEY(association_id, namespace_id) REFERENCES analytics_candidate_associations(id, namespace_id) ON DELETE CASCADE,
1011
+ FOREIGN KEY(person_id, site_id, namespace_id) REFERENCES analytics_people(id, site_id, namespace_id) ON DELETE CASCADE,
1012
+ UNIQUE(association_id, person_id)
1013
+ )
1014
+ `),await t.query(`
1015
+ CREATE TABLE IF NOT EXISTS analytics_identity_promotion_receipts (
1016
+ id uuid PRIMARY KEY,
1017
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1018
+ namespace_id uuid NOT NULL,
1019
+ canonical_person_id uuid NOT NULL,
1020
+ canonical_person_ref_hmac text NOT NULL,
1021
+ evidence_kind text NOT NULL,
1022
+ evidence_ref_hmac text NOT NULL,
1023
+ prior_person_ids uuid[] NOT NULL DEFAULT '{}',
1024
+ prior_identity_node_ids uuid[] NOT NULL DEFAULT '{}',
1025
+ candidate_association_ids uuid[] NOT NULL DEFAULT '{}',
1026
+ affected_history_counts jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(affected_history_counts)='object'),
1027
+ schema_version text NOT NULL DEFAULT 'xray.identity-promotion.v1',
1028
+ idempotency_key text NOT NULL,
1029
+ promoted_at timestamptz NOT NULL,
1030
+ created_at timestamptz NOT NULL DEFAULT now(),
1031
+ FOREIGN KEY(namespace_id, site_id) REFERENCES analytics_identity_namespaces(id, site_id) ON DELETE CASCADE,
1032
+ UNIQUE(site_id, idempotency_key)
1033
+ )
1034
+ `),await t.query("CREATE UNIQUE INDEX IF NOT EXISTS analytics_touches_site_ref ON analytics_attribution_touches(id, site_id)"),await t.query(`
1035
+ CREATE TABLE IF NOT EXISTS analytics_candidate_attribution_touches (
1036
+ id uuid PRIMARY KEY,
1037
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1038
+ namespace_id uuid NOT NULL,
1039
+ association_id uuid NOT NULL,
1040
+ touch_id uuid NOT NULL,
1041
+ eligibility text NOT NULL DEFAULT 'overlay_only' CHECK (eligibility IN ('overlay_only','review','ineligible','expired')),
1042
+ association_confidence numeric(5,4) NOT NULL CHECK (association_confidence >= 0 AND association_confidence <= 1),
1043
+ model_version text NOT NULL,
1044
+ linked_at timestamptz NOT NULL DEFAULT now(),
1045
+ expires_at timestamptz NOT NULL,
1046
+ FOREIGN KEY(namespace_id, site_id) REFERENCES analytics_identity_namespaces(id, site_id) ON DELETE CASCADE,
1047
+ FOREIGN KEY(association_id, namespace_id) REFERENCES analytics_candidate_associations(id, namespace_id) ON DELETE CASCADE,
1048
+ FOREIGN KEY(touch_id, site_id) REFERENCES analytics_attribution_touches(id, site_id) ON DELETE CASCADE,
1049
+ UNIQUE(association_id, touch_id, model_version)
1050
+ )
1051
+ `),await t.query(`
1052
+ CREATE TABLE IF NOT EXISTS analytics_event_value_definitions (
1053
+ id uuid PRIMARY KEY,
1054
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1055
+ event_definition_id uuid NOT NULL REFERENCES analytics_event_definitions(id) ON DELETE CASCADE,
1056
+ version integer NOT NULL CHECK (version > 0),
1057
+ base_mode text NOT NULL DEFAULT 'none' CHECK (base_mode IN ('none','fixed','dynamic')),
1058
+ fixed_value_minor bigint,
1059
+ currency text NOT NULL DEFAULT 'USD',
1060
+ dynamic_source jsonb,
1061
+ conditional_event_definition_id uuid REFERENCES analytics_event_definitions(id) ON DELETE SET NULL,
1062
+ conditional_window_days integer CHECK (conditional_window_days IS NULL OR conditional_window_days > 0),
1063
+ conditional_value_minor bigint,
1064
+ status text NOT NULL DEFAULT 'active' CHECK (status IN ('active','disabled','archived')),
1065
+ created_by_user_id bigint NOT NULL,
1066
+ created_at timestamptz NOT NULL DEFAULT now(),
1067
+ updated_at timestamptz NOT NULL DEFAULT now(),
1068
+ CHECK ((base_mode = 'fixed') = (fixed_value_minor IS NOT NULL)),
1069
+ CHECK ((conditional_event_definition_id IS NULL) = (conditional_value_minor IS NULL)),
1070
+ UNIQUE(site_id, event_definition_id, version)
1071
+ )
1072
+ `),await t.query(`
1073
+ CREATE TABLE IF NOT EXISTS analytics_event_value_evaluations (
1074
+ id uuid PRIMARY KEY,
1075
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1076
+ source_event_id uuid NOT NULL REFERENCES analytics_events(id) ON DELETE CASCADE,
1077
+ definition_id uuid REFERENCES analytics_event_value_definitions(id) ON DELETE SET NULL,
1078
+ definition_version integer,
1079
+ value_minor bigint,
1080
+ currency text,
1081
+ value_source text NOT NULL CHECK (value_source IN ('absent','default_zero','fixed','dynamic','conditional_override','provider','verified_revenue','restatement')),
1082
+ matched_event_id uuid REFERENCES analytics_events(id) ON DELETE SET NULL,
1083
+ provenance jsonb NOT NULL DEFAULT '{}'::jsonb,
1084
+ evaluated_at timestamptz NOT NULL DEFAULT now(),
1085
+ UNIQUE(site_id, source_event_id, definition_version)
1086
+ )
1087
+ `),await t.query(`
1088
+ CREATE TABLE IF NOT EXISTS analytics_data_class_policies (
1089
+ id uuid PRIMARY KEY,
1090
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1091
+ data_class text NOT NULL,
1092
+ purpose text NOT NULL,
1093
+ jurisdiction_profile text NOT NULL,
1094
+ retention_days integer CHECK (retention_days IS NULL OR retention_days > 0),
1095
+ storage_kind text NOT NULL CHECK (storage_kind IN ('ordinary','pseudonymous','encrypted_restricted','ephemeral')),
1096
+ withdrawal_action text NOT NULL CHECK (withdrawal_action IN ('stop_future','expire','delete','retain_necessary')),
1097
+ deletion_action text NOT NULL CHECK (deletion_action IN ('delete','anonymize','retain_legal')),
1098
+ read_surface text NOT NULL,
1099
+ version integer NOT NULL DEFAULT 1 CHECK (version > 0),
1100
+ created_at timestamptz NOT NULL DEFAULT now(),
1101
+ UNIQUE(site_id, data_class, jurisdiction_profile, version)
1102
+ )
1103
+ `),await t.query(`
1104
+ CREATE TABLE IF NOT EXISTS analytics_privacy_audits (
1105
+ id uuid PRIMARY KEY,
1106
+ site_id uuid REFERENCES analytics_sites(id) ON DELETE SET NULL,
1107
+ namespace_ref text,
1108
+ subject_ref text,
1109
+ action text NOT NULL CHECK (action IN ('export_requested','export_completed','deletion_requested','deletion_completed','withdrawal_applied','retention_expired','candidate_promoted')),
1110
+ idempotency_key text NOT NULL,
1111
+ actor_user_id bigint,
1112
+ result_code text NOT NULL,
1113
+ affected_counts jsonb NOT NULL DEFAULT '{}'::jsonb,
1114
+ created_at timestamptz NOT NULL DEFAULT now(),
1115
+ completed_at timestamptz,
1116
+ UNIQUE(site_id, idempotency_key)
1117
+ )
1118
+ `),await t.query("ALTER TABLE analytics_privacy_audits ADD COLUMN IF NOT EXISTS request_fingerprint text"),await t.query(`
1119
+ CREATE TABLE IF NOT EXISTS analytics_crm_provider_capabilities (
1120
+ provider text NOT NULL CHECK (provider IN ('hubspot','salesforce','highlevel','zoho','pipedrive','keap')),
1121
+ contract_version integer NOT NULL CHECK (contract_version > 0),
1122
+ capabilities jsonb NOT NULL DEFAULT '{}'::jsonb,
1123
+ documentation_refs jsonb NOT NULL DEFAULT '[]'::jsonb,
1124
+ created_at timestamptz NOT NULL DEFAULT now(),
1125
+ retired_at timestamptz,
1126
+ PRIMARY KEY(provider, contract_version)
1127
+ )
1128
+ `),await t.query(`
1129
+ CREATE TABLE IF NOT EXISTS analytics_crm_provisioning_plans (
1130
+ id uuid PRIMARY KEY,
1131
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1132
+ connection_id uuid NOT NULL REFERENCES analytics_connections(id) ON DELETE CASCADE,
1133
+ provider text NOT NULL CHECK (provider IN ('hubspot','salesforce','highlevel','zoho','pipedrive','keap')),
1134
+ contract_version integer NOT NULL CHECK (contract_version > 0),
1135
+ tenant_schema_version text,
1136
+ plan jsonb NOT NULL,
1137
+ status text NOT NULL DEFAULT 'preview' CHECK (status IN ('preview','approved','applying','verified','failed','reverted')),
1138
+ approved_by_user_id bigint,
1139
+ approved_at timestamptz,
1140
+ created_by_user_id bigint NOT NULL,
1141
+ created_at timestamptz NOT NULL DEFAULT now(),
1142
+ updated_at timestamptz NOT NULL DEFAULT now()
1143
+ )
1144
+ `),await t.query(`
1145
+ CREATE TABLE IF NOT EXISTS analytics_crm_provisioning_receipts (
1146
+ id uuid PRIMARY KEY,
1147
+ plan_id uuid NOT NULL REFERENCES analytics_crm_provisioning_plans(id) ON DELETE CASCADE,
1148
+ operation text NOT NULL CHECK (operation IN ('describe','discover','create_field','test_write','revert_test','verify')),
1149
+ status text NOT NULL CHECK (status IN ('accepted','rejected','failed','reverted')),
1150
+ external_object_ref text,
1151
+ safe_error_code text,
1152
+ warnings jsonb NOT NULL DEFAULT '[]'::jsonb,
1153
+ created_at timestamptz NOT NULL DEFAULT now()
1154
+ )
1155
+ `),await t.query(`
1156
+ CREATE TABLE IF NOT EXISTS analytics_crm_person_mappings (
1157
+ id uuid PRIMARY KEY,
1158
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1159
+ namespace_id uuid NOT NULL,
1160
+ connection_id uuid NOT NULL REFERENCES analytics_connections(id) ON DELETE CASCADE,
1161
+ person_id uuid NOT NULL,
1162
+ provider_person_ref text NOT NULL,
1163
+ mapping_version integer NOT NULL DEFAULT 1 CHECK (mapping_version > 0),
1164
+ last_synced_at timestamptz,
1165
+ created_at timestamptz NOT NULL DEFAULT now(),
1166
+ updated_at timestamptz NOT NULL DEFAULT now(),
1167
+ FOREIGN KEY(namespace_id, site_id) REFERENCES analytics_identity_namespaces(id, site_id) ON DELETE CASCADE,
1168
+ FOREIGN KEY(person_id, site_id, namespace_id) REFERENCES analytics_people(id, site_id, namespace_id) ON DELETE CASCADE,
1169
+ UNIQUE(connection_id, person_id),
1170
+ UNIQUE(connection_id, provider_person_ref)
1171
+ )
1172
+ `),await t.query(`
1173
+ CREATE TABLE IF NOT EXISTS analytics_crm_pipeline_mappings (
1174
+ id uuid PRIMARY KEY,
1175
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1176
+ connection_id uuid NOT NULL REFERENCES analytics_connections(id) ON DELETE CASCADE,
1177
+ funnel_definition_id uuid REFERENCES analytics_funnel_definitions(id) ON DELETE SET NULL,
1178
+ version integer NOT NULL DEFAULT 1 CHECK (version > 0),
1179
+ mapping jsonb NOT NULL,
1180
+ status text NOT NULL DEFAULT 'draft' CHECK (status IN ('draft','active','disabled','archived')),
1181
+ created_by_user_id bigint NOT NULL,
1182
+ created_at timestamptz NOT NULL DEFAULT now(),
1183
+ updated_at timestamptz NOT NULL DEFAULT now(),
1184
+ UNIQUE(connection_id, version)
1185
+ )
1186
+ `),await t.query(`
1187
+ CREATE TABLE IF NOT EXISTS analytics_crm_sync_state (
1188
+ connection_id uuid PRIMARY KEY REFERENCES analytics_connections(id) ON DELETE CASCADE,
1189
+ provisioning_plan_id uuid REFERENCES analytics_crm_provisioning_plans(id) ON DELETE SET NULL,
1190
+ cursor jsonb,
1191
+ direction text NOT NULL DEFAULT 'bidirectional' CHECK (direction IN ('inbound','outbound','bidirectional','webhook_only')),
1192
+ status text NOT NULL DEFAULT 'idle' CHECK (status IN ('idle','pending','running','healthy','degraded','disabled')),
1193
+ last_started_at timestamptz,
1194
+ last_completed_at timestamptz,
1195
+ last_success_at timestamptz,
1196
+ last_error_code text,
1197
+ next_sync_at timestamptz,
1198
+ updated_at timestamptz NOT NULL DEFAULT now()
1199
+ )
1200
+ `),await t.query(`
1201
+ CREATE TABLE IF NOT EXISTS analytics_crm_sync_receipts (
1202
+ id uuid PRIMARY KEY,
1203
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1204
+ connection_id uuid NOT NULL REFERENCES analytics_connections(id) ON DELETE CASCADE,
1205
+ direction text NOT NULL CHECK (direction IN ('inbound','outbound')),
1206
+ operation text NOT NULL CHECK (operation IN ('person_upsert','activity_append','pipeline_update','schema_read','historical_pull')),
1207
+ subject_ref text,
1208
+ external_receipt_id text,
1209
+ status text NOT NULL CHECK (status IN ('accepted','rejected','failed','retrying')),
1210
+ safe_error_code text,
1211
+ occurred_at timestamptz NOT NULL DEFAULT now()
1212
+ )
1213
+ `),await t.query("ALTER TABLE analytics_crm_provisioning_plans ADD COLUMN IF NOT EXISTS public_plan_id text"),await t.query("ALTER TABLE analytics_crm_provisioning_plans ADD COLUMN IF NOT EXISTS schema_fingerprint text"),await t.query("ALTER TABLE analytics_crm_provisioning_plans ADD COLUMN IF NOT EXISTS request_fingerprint text"),await t.query("ALTER TABLE analytics_crm_provisioning_plans ADD COLUMN IF NOT EXISTS approved_field_ids jsonb NOT NULL DEFAULT '[]'::jsonb"),await t.query("ALTER TABLE analytics_crm_provisioning_receipts ADD COLUMN IF NOT EXISTS public_receipt_id text"),await t.query("ALTER TABLE analytics_crm_provisioning_receipts ADD COLUMN IF NOT EXISTS receipt jsonb NOT NULL DEFAULT '{}'::jsonb"),await t.query("CREATE UNIQUE INDEX IF NOT EXISTS analytics_crm_plan_public_scope ON analytics_crm_provisioning_plans(site_id, public_plan_id) WHERE public_plan_id IS NOT NULL"),await t.query("CREATE UNIQUE INDEX IF NOT EXISTS analytics_crm_receipt_public_id ON analytics_crm_provisioning_receipts(public_receipt_id) WHERE public_receipt_id IS NOT NULL"),await t.query(`
1214
+ CREATE TABLE IF NOT EXISTS analytics_crm_connection_capabilities (
1215
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1216
+ connection_id uuid NOT NULL REFERENCES analytics_connections(id) ON DELETE CASCADE,
1217
+ provider text NOT NULL CHECK (provider IN ('hubspot','salesforce','highlevel','zoho','pipedrive','keap')),
1218
+ contract_version integer NOT NULL DEFAULT 1 CHECK (contract_version > 0),
1219
+ mode text NOT NULL CHECK (mode IN ('incremental_and_webhook','incremental_only','webhook_only','manual_backfill','unavailable')),
1220
+ schema_fingerprint text,
1221
+ allowed_operations jsonb NOT NULL DEFAULT '[]'::jsonb,
1222
+ evidence jsonb NOT NULL,
1223
+ verified_at timestamptz NOT NULL,
1224
+ updated_at timestamptz NOT NULL DEFAULT now(),
1225
+ PRIMARY KEY(connection_id, contract_version),
1226
+ UNIQUE(site_id, connection_id, provider, contract_version)
1227
+ )
1228
+ `),await t.query(`
1229
+ CREATE TABLE IF NOT EXISTS analytics_crm_sync_runs (
1230
+ sync_id text PRIMARY KEY,
1231
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1232
+ connection_id uuid NOT NULL REFERENCES analytics_connections(id) ON DELETE CASCADE,
1233
+ provider text NOT NULL CHECK (provider IN ('hubspot','salesforce','highlevel','zoho','pipedrive','keap')),
1234
+ mode text NOT NULL CHECK (mode IN ('initial','incremental','webhook')),
1235
+ state text NOT NULL DEFAULT 'queued' CHECK (state IN ('queued','running','completed','degraded','cancelled','webhook_only')),
1236
+ mapping jsonb NOT NULL,
1237
+ mapping_version integer NOT NULL DEFAULT 1 CHECK (mapping_version > 0),
1238
+ capability_evidence jsonb NOT NULL DEFAULT '{}'::jsonb,
1239
+ cursor jsonb,
1240
+ idempotency_key text NOT NULL,
1241
+ request_fingerprint text NOT NULL,
1242
+ last_started_at timestamptz,
1243
+ last_success_at timestamptz,
1244
+ next_run_at timestamptz,
1245
+ interval_seconds integer CHECK (interval_seconds IS NULL OR (interval_seconds >= 900 AND interval_seconds <= 604800)),
1246
+ imported integer NOT NULL DEFAULT 0 CHECK (imported >= 0),
1247
+ deduplicated integer NOT NULL DEFAULT 0 CHECK (deduplicated >= 0),
1248
+ rejected integer NOT NULL DEFAULT 0 CHECK (rejected >= 0),
1249
+ last_error_code text,
1250
+ created_at timestamptz NOT NULL DEFAULT now(),
1251
+ updated_at timestamptz NOT NULL DEFAULT now(),
1252
+ UNIQUE(site_id, idempotency_key)
1253
+ )
1254
+ `),await t.query("ALTER TABLE analytics_crm_sync_runs ADD COLUMN IF NOT EXISTS interval_seconds integer CHECK (interval_seconds IS NULL OR (interval_seconds >= 900 AND interval_seconds <= 604800))"),await t.query("CREATE INDEX IF NOT EXISTS analytics_crm_sync_runs_due ON analytics_crm_sync_runs(state, next_run_at) WHERE next_run_at IS NOT NULL"),await t.query(`
1255
+ CREATE TABLE IF NOT EXISTS analytics_crm_outbound_policies (
1256
+ policy_id text PRIMARY KEY,
1257
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1258
+ connection_id uuid NOT NULL REFERENCES analytics_connections(id) ON DELETE CASCADE,
1259
+ provider text NOT NULL CHECK (provider IN ('hubspot','salesforce','highlevel','zoho','pipedrive','keap')),
1260
+ kind text NOT NULL CHECK (kind IN ('person_summary','pipeline_event')),
1261
+ enabled boolean NOT NULL DEFAULT false,
1262
+ version integer NOT NULL CHECK (version > 0),
1263
+ provisioning_receipt_id text NOT NULL,
1264
+ schema_fingerprint text NOT NULL,
1265
+ mapping_version integer CHECK (mapping_version > 0),
1266
+ mapping jsonb,
1267
+ allowed_fields jsonb NOT NULL DEFAULT '[]'::jsonb CHECK (jsonb_typeof(allowed_fields) = 'array'),
1268
+ selected_events jsonb NOT NULL DEFAULT '[]'::jsonb CHECK (jsonb_typeof(selected_events) = 'array'),
1269
+ purpose text NOT NULL CHECK (purpose IN ('necessary_form_fulfillment','site_analytics','advertising_measurement')),
1270
+ created_by_user_id bigint NOT NULL,
1271
+ created_at timestamptz NOT NULL DEFAULT now(),
1272
+ updated_at timestamptz NOT NULL DEFAULT now(),
1273
+ UNIQUE(site_id, connection_id, kind, version),
1274
+ CHECK ((kind='person_summary' AND mapping_version IS NULL AND mapping IS NULL) OR
1275
+ (kind='pipeline_event' AND mapping_version IS NOT NULL AND mapping IS NOT NULL))
1276
+ )
1277
+ `),await t.query(`
1278
+ CREATE TABLE IF NOT EXISTS analytics_crm_outbound_jobs (
1279
+ job_id text PRIMARY KEY,
1280
+ policy_id text NOT NULL REFERENCES analytics_crm_outbound_policies(policy_id) ON DELETE CASCADE,
1281
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1282
+ person_id uuid NOT NULL REFERENCES analytics_people(id) ON DELETE CASCADE,
1283
+ kind text NOT NULL CHECK (kind IN ('person_summary','pipeline_event')),
1284
+ state text NOT NULL DEFAULT 'pending' CHECK (state IN ('pending','running','retrying','delivered','dead_letter','cancelled')),
1285
+ policy_version integer NOT NULL CHECK (policy_version > 0),
1286
+ mapping_version integer CHECK (mapping_version > 0),
1287
+ source_event_id text,
1288
+ payload_ciphertext text NOT NULL,
1289
+ idempotency_key text NOT NULL,
1290
+ request_fingerprint text NOT NULL,
1291
+ attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0),
1292
+ max_attempts integer NOT NULL DEFAULT 5 CHECK (max_attempts BETWEEN 1 AND 20),
1293
+ next_attempt_at timestamptz,
1294
+ lease_expires_at timestamptz,
1295
+ safe_error_code text,
1296
+ created_at timestamptz NOT NULL DEFAULT now(),
1297
+ updated_at timestamptz NOT NULL DEFAULT now(),
1298
+ delivered_at timestamptz,
1299
+ cancelled_at timestamptz,
1300
+ UNIQUE(site_id, idempotency_key)
1301
+ )
1302
+ `),await t.query(`
1303
+ CREATE TABLE IF NOT EXISTS analytics_crm_outbound_attempts (
1304
+ id uuid PRIMARY KEY,
1305
+ job_id text NOT NULL REFERENCES analytics_crm_outbound_jobs(job_id) ON DELETE CASCADE,
1306
+ attempt integer NOT NULL CHECK (attempt > 0),
1307
+ status text NOT NULL CHECK (status IN ('started','accepted','retrying','failed','cancelled')),
1308
+ safe_error_code text,
1309
+ started_at timestamptz NOT NULL DEFAULT now(),
1310
+ completed_at timestamptz,
1311
+ UNIQUE(job_id, attempt)
1312
+ )
1313
+ `),await t.query(`
1314
+ CREATE TABLE IF NOT EXISTS analytics_crm_outbound_receipts (
1315
+ receipt_id text PRIMARY KEY,
1316
+ job_id text NOT NULL REFERENCES analytics_crm_outbound_jobs(job_id) ON DELETE CASCADE,
1317
+ status text NOT NULL CHECK (status IN ('accepted','rejected','failed','retrying')),
1318
+ external_receipt_id text,
1319
+ safe_error_code text,
1320
+ occurred_at timestamptz NOT NULL DEFAULT now()
1321
+ )
1322
+ `),await t.query(`
1323
+ CREATE TABLE IF NOT EXISTS analytics_crm_outbound_worker_state (
1324
+ worker_key text PRIMARY KEY,
1325
+ last_started_at timestamptz,
1326
+ last_completed_at timestamptz,
1327
+ last_success_at timestamptz,
1328
+ last_error_code text,
1329
+ last_claimed integer NOT NULL DEFAULT 0,
1330
+ last_remaining_due integer NOT NULL DEFAULT 0,
1331
+ updated_at timestamptz NOT NULL DEFAULT now()
1332
+ )
1333
+ `),await t.query("CREATE INDEX IF NOT EXISTS analytics_crm_outbound_jobs_due ON analytics_crm_outbound_jobs(state,next_attempt_at) WHERE state IN ('pending','retrying')"),await t.query("CREATE INDEX IF NOT EXISTS analytics_crm_outbound_jobs_person ON analytics_crm_outbound_jobs(site_id,person_id,created_at DESC)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_crm_outbound_receipts_job ON analytics_crm_outbound_receipts(job_id,occurred_at DESC)"),await t.query(`
1334
+ CREATE TABLE IF NOT EXISTS analytics_score_definitions (
1335
+ id uuid PRIMARY KEY,
1336
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1337
+ name text NOT NULL,
1338
+ score_kind text NOT NULL CHECK (score_kind IN ('fit','engagement','combined')),
1339
+ rules jsonb NOT NULL CHECK (jsonb_typeof(rules) = 'array'),
1340
+ thresholds jsonb NOT NULL DEFAULT '[]'::jsonb CHECK (jsonb_typeof(thresholds) = 'array'),
1341
+ minimum_score integer NOT NULL DEFAULT 0,
1342
+ maximum_score integer NOT NULL DEFAULT 100 CHECK (maximum_score >= minimum_score),
1343
+ version integer NOT NULL DEFAULT 1 CHECK (version > 0),
1344
+ status text NOT NULL DEFAULT 'active' CHECK (status IN ('active','disabled','archived')),
1345
+ created_by_user_id bigint NOT NULL,
1346
+ created_at timestamptz NOT NULL DEFAULT now(),
1347
+ updated_at timestamptz NOT NULL DEFAULT now(),
1348
+ UNIQUE(site_id, name, version)
1349
+ )
1350
+ `),await t.query(`
1351
+ CREATE TABLE IF NOT EXISTS analytics_score_evaluations (
1352
+ id uuid PRIMARY KEY,
1353
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1354
+ namespace_id uuid NOT NULL,
1355
+ definition_id uuid NOT NULL REFERENCES analytics_score_definitions(id) ON DELETE CASCADE,
1356
+ definition_version integer NOT NULL CHECK (definition_version > 0),
1357
+ person_id uuid,
1358
+ candidate_association_id uuid,
1359
+ score integer NOT NULL,
1360
+ band text,
1361
+ contributions jsonb NOT NULL DEFAULT '[]'::jsonb,
1362
+ evaluated_at timestamptz NOT NULL DEFAULT now(),
1363
+ FOREIGN KEY(namespace_id, site_id) REFERENCES analytics_identity_namespaces(id, site_id) ON DELETE CASCADE,
1364
+ FOREIGN KEY(person_id, site_id, namespace_id) REFERENCES analytics_people(id, site_id, namespace_id) ON DELETE CASCADE,
1365
+ FOREIGN KEY(candidate_association_id, namespace_id) REFERENCES analytics_candidate_associations(id, namespace_id) ON DELETE CASCADE,
1366
+ CHECK ((person_id IS NOT NULL)::integer + (candidate_association_id IS NOT NULL)::integer = 1)
1367
+ )
1368
+ `),await t.query(`
1369
+ CREATE TABLE IF NOT EXISTS analytics_prediction_targets (
1370
+ id uuid PRIMARY KEY,
1371
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1372
+ namespace_id uuid NOT NULL,
1373
+ name text NOT NULL,
1374
+ target_event_definition_id uuid NOT NULL REFERENCES analytics_event_definitions(id) ON DELETE RESTRICT,
1375
+ horizon_days integer NOT NULL CHECK (horizon_days > 0),
1376
+ minimum_confirmed_people integer NOT NULL CHECK (minimum_confirmed_people > 0),
1377
+ minimum_positive_outcomes integer NOT NULL CHECK (minimum_positive_outcomes > 0),
1378
+ status text NOT NULL DEFAULT 'draft' CHECK (status IN ('draft','eligible','training','active','degraded','disabled')),
1379
+ created_by_user_id bigint NOT NULL,
1380
+ created_at timestamptz NOT NULL DEFAULT now(),
1381
+ updated_at timestamptz NOT NULL DEFAULT now(),
1382
+ FOREIGN KEY(namespace_id, site_id) REFERENCES analytics_identity_namespaces(id, site_id) ON DELETE CASCADE,
1383
+ UNIQUE(id, site_id, namespace_id),
1384
+ UNIQUE(namespace_id, name)
1385
+ )
1386
+ `),await t.query(`
1387
+ CREATE TABLE IF NOT EXISTS analytics_prediction_manifests (
1388
+ id uuid PRIMARY KEY,
1389
+ target_id uuid NOT NULL REFERENCES analytics_prediction_targets(id) ON DELETE CASCADE,
1390
+ model_version text NOT NULL,
1391
+ training_window_start timestamptz NOT NULL,
1392
+ training_window_end timestamptz NOT NULL,
1393
+ confirmed_people_count integer NOT NULL CHECK (confirmed_people_count >= 0),
1394
+ positive_outcome_count integer NOT NULL CHECK (positive_outcome_count >= 0),
1395
+ calibration jsonb NOT NULL DEFAULT '{}'::jsonb,
1396
+ feature_manifest jsonb NOT NULL DEFAULT '{}'::jsonb,
1397
+ status text NOT NULL CHECK (status IN ('ineligible','training','validated','active','degraded','retired')),
1398
+ trained_at timestamptz,
1399
+ activated_at timestamptz,
1400
+ retired_at timestamptz,
1401
+ created_at timestamptz NOT NULL DEFAULT now(),
1402
+ UNIQUE(target_id, model_version),
1403
+ UNIQUE(id, target_id)
1404
+ )
1405
+ `),await t.query(`
1406
+ CREATE TABLE IF NOT EXISTS analytics_prediction_results (
1407
+ id uuid PRIMARY KEY,
1408
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1409
+ namespace_id uuid NOT NULL,
1410
+ target_id uuid NOT NULL,
1411
+ manifest_id uuid NOT NULL,
1412
+ person_id uuid NOT NULL,
1413
+ probability numeric(7,6) NOT NULL CHECK (probability >= 0 AND probability <= 1),
1414
+ expected_value_minor bigint,
1415
+ currency text,
1416
+ status text NOT NULL DEFAULT 'available' CHECK (status IN ('available','stale','superseded','deleted')),
1417
+ evaluated_at timestamptz NOT NULL DEFAULT now(),
1418
+ expires_at timestamptz NOT NULL,
1419
+ FOREIGN KEY(namespace_id, site_id) REFERENCES analytics_identity_namespaces(id, site_id) ON DELETE CASCADE,
1420
+ FOREIGN KEY(target_id, site_id, namespace_id) REFERENCES analytics_prediction_targets(id, site_id, namespace_id) ON DELETE CASCADE,
1421
+ FOREIGN KEY(manifest_id, target_id) REFERENCES analytics_prediction_manifests(id, target_id) ON DELETE CASCADE,
1422
+ FOREIGN KEY(person_id, site_id, namespace_id) REFERENCES analytics_people(id, site_id, namespace_id) ON DELETE CASCADE,
1423
+ UNIQUE(manifest_id, person_id)
1424
+ )
1425
+ `),await t.query(`
1426
+ CREATE TABLE IF NOT EXISTS analytics_ad_spend_facts (
1427
+ id uuid PRIMARY KEY,
1428
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1429
+ connection_id uuid REFERENCES analytics_connections(id) ON DELETE SET NULL,
1430
+ bucket date NOT NULL,
1431
+ platform text NOT NULL,
1432
+ account_id text,
1433
+ campaign_id text,
1434
+ campaign_name text,
1435
+ ad_set_id text,
1436
+ ad_set_name text,
1437
+ ad_id text,
1438
+ ad_name text,
1439
+ creative_id text,
1440
+ placement text,
1441
+ spend_minor bigint NOT NULL CHECK (spend_minor >= 0),
1442
+ currency text NOT NULL,
1443
+ source_ref text NOT NULL,
1444
+ source_updated_at timestamptz,
1445
+ created_at timestamptz NOT NULL DEFAULT now(),
1446
+ updated_at timestamptz NOT NULL DEFAULT now(),
1447
+ UNIQUE(site_id, platform, bucket, source_ref)
1448
+ )
1449
+ `),await t.query(`CREATE UNIQUE INDEX IF NOT EXISTS analytics_exports_owner_idempotency
1450
+ ON analytics_exports(requested_by_user_id, idempotency_key) WHERE idempotency_key IS NOT NULL`),await t.query("CREATE INDEX IF NOT EXISTS analytics_events_site_time ON analytics_events(site_id, occurred_at DESC)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_events_site_session_hmac_time ON analytics_events(site_id, session_hmac, occurred_at DESC)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_events_site_visitor_hmac_time ON analytics_events(site_id, visitor_hmac, occurred_at DESC)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_conversions_site_session_hmac_time ON analytics_conversions(site_id, session_hmac, occurred_at DESC)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_events_pixel_time ON analytics_events(pixel_id, occurred_at DESC)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_events_site_hostname_time ON analytics_events(site_id, hostname, occurred_at DESC)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_events_acquisition ON analytics_events(site_id, source, medium, campaign, occurred_at DESC)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_conversions_site_time ON analytics_conversions(site_id, occurred_at DESC)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_sessions_site_visitor_time ON analytics_sessions(site_id, visitor_hmac, started_at DESC)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_touches_site_person_time ON analytics_attribution_touches(site_id, person_id, occurred_at)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_external_events_site_time ON analytics_external_events(site_id, occurred_at DESC)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_webhook_receipts_reconcile ON analytics_webhook_receipts(connection_id, received_at DESC)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_reconciliation_jobs_due ON analytics_reconciliation_jobs(status, next_attempt_at, lease_until)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_activation_jobs_due ON analytics_activation_jobs(status, next_attempt_at) WHERE status = 'pending'"),await t.query("CREATE INDEX IF NOT EXISTS analytics_activation_tests_destination_time ON analytics_activation_destination_tests(destination_id, created_at DESC)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_activation_attempts_destination_time ON analytics_activation_delivery_attempts(destination_id, created_at DESC)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_activation_tests_diagnostics_due ON analytics_activation_destination_tests(next_diagnostic_at) WHERE next_diagnostic_at IS NOT NULL"),await t.query("CREATE INDEX IF NOT EXISTS analytics_restricted_data_expiry ON analytics_restricted_data(expires_at)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_linker_tokens_expiry ON analytics_linker_tokens(expires_at) WHERE consumed_at IS NULL"),await t.query("CREATE UNIQUE INDEX IF NOT EXISTS analytics_sessions_site_public_ref ON analytics_sessions(site_id, public_ref) WHERE public_ref IS NOT NULL"),await t.query("CREATE UNIQUE INDEX IF NOT EXISTS analytics_people_site_public_ref ON analytics_people(site_id, public_ref) WHERE public_ref IS NOT NULL"),await t.query("CREATE INDEX IF NOT EXISTS analytics_sessions_keyset ON analytics_sessions(site_id, started_at DESC, id DESC)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_events_keyset ON analytics_events(site_id, occurred_at DESC, id DESC)"),await t.query("CREATE UNIQUE INDEX IF NOT EXISTS analytics_funnels_one_default ON analytics_funnel_definitions(site_id) WHERE is_default AND status = 'active'"),await t.query("CREATE UNIQUE INDEX IF NOT EXISTS analytics_saved_views_one_default ON analytics_saved_views(site_id, owner_user_id, report_type) WHERE is_default"),await t.query("CREATE INDEX IF NOT EXISTS analytics_product_milestones_site_time ON analytics_product_milestones(site_id, occurred_at DESC)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_scheduled_occurrences_due ON analytics_scheduled_occurrences(status,next_attempt_at,scheduled_for) WHERE status IN ('pending','retrying')"),await t.query("CREATE INDEX IF NOT EXISTS analytics_scheduled_occurrences_lease ON analytics_scheduled_occurrences(lease_until) WHERE status='running'"),await t.query("CREATE INDEX IF NOT EXISTS analytics_consent_receipts_subject_time ON analytics_consent_receipts(site_id, subject_ref, occurred_at DESC)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_consent_receipts_expiry ON analytics_consent_receipts(expires_at) WHERE expires_at IS NOT NULL"),await t.query("CREATE INDEX IF NOT EXISTS analytics_consent_current_expiry ON analytics_consent_current_state(expires_at) WHERE expires_at IS NOT NULL"),await t.query("CREATE INDEX IF NOT EXISTS analytics_identity_sources_site ON analytics_identity_sources(site_id, namespace_id, status)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_candidates_active_expiry ON analytics_candidate_associations(namespace_id, expires_at) WHERE status = 'active'"),await t.query("CREATE INDEX IF NOT EXISTS analytics_candidates_review ON analytics_candidate_associations(namespace_id, confidence_band, updated_at DESC) WHERE status = 'active'"),await t.query("CREATE INDEX IF NOT EXISTS analytics_candidate_evidence_expiry ON analytics_candidate_evidence(expires_at)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_candidate_promotions_person_time ON analytics_candidate_promotions(site_id, person_id, promoted_at DESC)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_identity_promotion_receipts_person_time ON analytics_identity_promotion_receipts(site_id, canonical_person_id, promoted_at DESC)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_candidate_touches_overlay ON analytics_candidate_attribution_touches(site_id, namespace_id, linked_at DESC) WHERE eligibility = 'overlay_only'"),await t.query("CREATE INDEX IF NOT EXISTS analytics_candidate_touches_expiry ON analytics_candidate_attribution_touches(expires_at) WHERE eligibility IN ('overlay_only','review')"),await t.query("CREATE INDEX IF NOT EXISTS analytics_value_evaluations_event ON analytics_event_value_evaluations(site_id, source_event_id)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_privacy_audits_site_time ON analytics_privacy_audits(site_id, created_at DESC)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_crm_provisioning_connection_time ON analytics_crm_provisioning_plans(connection_id, created_at DESC)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_crm_sync_due ON analytics_crm_sync_state(status, next_sync_at) WHERE next_sync_at IS NOT NULL"),await t.query("CREATE INDEX IF NOT EXISTS analytics_crm_sync_receipts_connection_time ON analytics_crm_sync_receipts(connection_id, occurred_at DESC)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_scores_person_time ON analytics_score_evaluations(site_id, person_id, evaluated_at DESC) WHERE person_id IS NOT NULL"),await t.query("CREATE INDEX IF NOT EXISTS analytics_scores_candidate_time ON analytics_score_evaluations(site_id, candidate_association_id, evaluated_at DESC) WHERE candidate_association_id IS NOT NULL"),await t.query("CREATE INDEX IF NOT EXISTS analytics_prediction_results_person_time ON analytics_prediction_results(site_id, person_id, evaluated_at DESC)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_prediction_results_expiry ON analytics_prediction_results(expires_at) WHERE status = 'available'"),await t.query("CREATE INDEX IF NOT EXISTS analytics_touches_hierarchy ON analytics_attribution_touches(site_id, platform, account_id, campaign_id, ad_set_id, ad_id, occurred_at DESC)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_spend_facts_hierarchy ON analytics_ad_spend_facts(site_id, platform, account_id, campaign_id, ad_set_id, ad_id, bucket DESC)"),await t.query(`
1451
+ CREATE TABLE IF NOT EXISTS analytics_runtime_manifests (
1452
+ manifest_id uuid PRIMARY KEY,
1453
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1454
+ revision integer NOT NULL CHECK (revision > 0),
1455
+ digest text NOT NULL CHECK (digest ~ '^[a-f0-9]{64}$'),
1456
+ manifest jsonb NOT NULL,
1457
+ is_current boolean NOT NULL DEFAULT false,
1458
+ created_by_user_id bigint NOT NULL,
1459
+ expires_at timestamptz NOT NULL,
1460
+ created_at timestamptz NOT NULL DEFAULT now(),
1461
+ UNIQUE(site_id, revision)
1462
+ )
1463
+ `),await t.query("CREATE UNIQUE INDEX IF NOT EXISTS analytics_runtime_manifests_one_current ON analytics_runtime_manifests(site_id) WHERE is_current"),await t.query("CREATE INDEX IF NOT EXISTS analytics_runtime_manifests_site_created ON analytics_runtime_manifests(site_id, created_at DESC)"),await t.query(`
1464
+ CREATE TABLE IF NOT EXISTS analytics_runtime_manifest_publications (
1465
+ publication_id uuid PRIMARY KEY,
1466
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1467
+ manifest_id uuid NOT NULL REFERENCES analytics_runtime_manifests(manifest_id) ON DELETE CASCADE,
1468
+ setup_id uuid,
1469
+ setup_revision integer,
1470
+ idempotency_key text NOT NULL,
1471
+ request_fingerprint text NOT NULL CHECK (request_fingerprint ~ '^[a-f0-9]{64}$'),
1472
+ manifest jsonb NOT NULL,
1473
+ published_at timestamptz NOT NULL DEFAULT now(),
1474
+ UNIQUE(site_id, idempotency_key)
1475
+ )
1476
+ `),await t.query(`
1477
+ CREATE TABLE IF NOT EXISTS analytics_runtime_rule_health (
1478
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1479
+ manifest_id uuid NOT NULL REFERENCES analytics_runtime_manifests(manifest_id) ON DELETE CASCADE,
1480
+ rule_id uuid NOT NULL,
1481
+ rule_version integer NOT NULL,
1482
+ route_path text NOT NULL,
1483
+ selector_quality text NOT NULL CHECK (selector_quality IN ('portable','stable','fragile')),
1484
+ match_count integer NOT NULL CHECK (match_count BETWEEN 0 AND 100),
1485
+ structural_fingerprint text NOT NULL CHECK (structural_fingerprint ~ '^[a-f0-9]{64}$'),
1486
+ observed_at timestamptz NOT NULL,
1487
+ PRIMARY KEY(manifest_id, rule_id, route_path)
1488
+ )
1489
+ `),await t.query(`
1490
+ CREATE TABLE IF NOT EXISTS analytics_experiments (
1491
+ experiment_id uuid PRIMARY KEY,
1492
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1493
+ current_revision integer NOT NULL CHECK (current_revision > 0),
1494
+ state text NOT NULL CHECK (state IN ('draft','approved','canary','active','paused','killed','archived')),
1495
+ created_by_user_id bigint NOT NULL,
1496
+ create_idempotency_key text,
1497
+ create_request_fingerprint text,
1498
+ started_at timestamptz,
1499
+ created_at timestamptz NOT NULL DEFAULT now(),
1500
+ updated_at timestamptz NOT NULL DEFAULT now()
1501
+ )
1502
+ `),await t.query("ALTER TABLE analytics_experiments ADD COLUMN IF NOT EXISTS create_idempotency_key text"),await t.query("ALTER TABLE analytics_experiments ADD COLUMN IF NOT EXISTS create_request_fingerprint text"),await t.query("ALTER TABLE analytics_experiments ADD COLUMN IF NOT EXISTS started_at timestamptz"),await t.query("CREATE UNIQUE INDEX IF NOT EXISTS analytics_experiments_site_create_idempotency ON analytics_experiments(site_id, create_idempotency_key) WHERE create_idempotency_key IS NOT NULL"),await t.query(`
1503
+ CREATE TABLE IF NOT EXISTS analytics_experiment_revisions (
1504
+ experiment_id uuid NOT NULL REFERENCES analytics_experiments(experiment_id) ON DELETE CASCADE,
1505
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1506
+ revision integer NOT NULL CHECK (revision > 0),
1507
+ plan jsonb NOT NULL,
1508
+ approved_by_user_id bigint,
1509
+ approved_at timestamptz,
1510
+ created_at timestamptz NOT NULL DEFAULT now(),
1511
+ PRIMARY KEY(experiment_id, revision)
1512
+ )
1513
+ `),await t.query(`
1514
+ CREATE TABLE IF NOT EXISTS analytics_experiment_receipts (
1515
+ receipt_id uuid PRIMARY KEY,
1516
+ experiment_id uuid NOT NULL REFERENCES analytics_experiments(experiment_id) ON DELETE CASCADE,
1517
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1518
+ revision integer NOT NULL,
1519
+ operation text NOT NULL,
1520
+ idempotency_key text,
1521
+ receipt jsonb NOT NULL,
1522
+ occurred_at timestamptz NOT NULL DEFAULT now(),
1523
+ UNIQUE(experiment_id, idempotency_key)
1524
+ )
1525
+ `),await t.query("CREATE INDEX IF NOT EXISTS analytics_experiments_site_updated ON analytics_experiments(site_id, updated_at DESC)"),await t.query(`
1526
+ CREATE TABLE IF NOT EXISTS analytics_site_setups (
1527
+ setup_id uuid PRIMARY KEY,
1528
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1529
+ owner_user_id bigint NOT NULL,
1530
+ revision integer NOT NULL CHECK (revision > 0),
1531
+ status text NOT NULL,
1532
+ start_url text NOT NULL,
1533
+ state jsonb NOT NULL,
1534
+ expires_at timestamptz NOT NULL,
1535
+ created_at timestamptz NOT NULL,
1536
+ updated_at timestamptz NOT NULL
1537
+ )
1538
+ `),await t.query(`
1539
+ CREATE TABLE IF NOT EXISTS analytics_site_setup_revisions (
1540
+ setup_id uuid NOT NULL REFERENCES analytics_site_setups(setup_id) ON DELETE CASCADE,
1541
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1542
+ owner_user_id bigint NOT NULL,
1543
+ revision integer NOT NULL CHECK (revision > 0),
1544
+ status text NOT NULL,
1545
+ state jsonb NOT NULL,
1546
+ created_at timestamptz NOT NULL,
1547
+ PRIMARY KEY(setup_id, revision)
1548
+ )
1549
+ `),await t.query(`
1550
+ CREATE TABLE IF NOT EXISTS analytics_site_setup_receipts (
1551
+ receipt_id uuid PRIMARY KEY,
1552
+ setup_id uuid NOT NULL REFERENCES analytics_site_setups(setup_id) ON DELETE CASCADE,
1553
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1554
+ owner_user_id bigint NOT NULL,
1555
+ revision integer NOT NULL,
1556
+ operation text NOT NULL,
1557
+ status text NOT NULL,
1558
+ receipt jsonb NOT NULL,
1559
+ occurred_at timestamptz NOT NULL
1560
+ )
1561
+ `),await t.query("CREATE INDEX IF NOT EXISTS analytics_site_setups_owner_updated ON analytics_site_setups(owner_user_id, updated_at DESC)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_site_setups_site_updated ON analytics_site_setups(site_id, updated_at DESC)"),await t.query("CREATE INDEX IF NOT EXISTS analytics_site_setup_receipts_setup_revision ON analytics_site_setup_receipts(setup_id, revision, occurred_at)"),await t.query("ALTER TABLE analytics_conversions ADD COLUMN IF NOT EXISTS authority_key text"),await t.query("ALTER TABLE analytics_conversions ADD COLUMN IF NOT EXISTS authority_id uuid"),await t.query("ALTER TABLE analytics_conversions ADD COLUMN IF NOT EXISTS authority_version_id uuid"),await t.query("ALTER TABLE analytics_conversions ADD COLUMN IF NOT EXISTS restated_at timestamptz"),await t.query(`CREATE UNIQUE INDEX IF NOT EXISTS analytics_conversions_site_authority_key
1562
+ ON analytics_conversions(site_id, authority_key) WHERE authority_key IS NOT NULL`),await t.query(`
1563
+ CREATE TABLE IF NOT EXISTS analytics_revenue_authorities (
1564
+ authority_id uuid PRIMARY KEY,
1565
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1566
+ provider text NOT NULL,
1567
+ source_account_ref text NOT NULL,
1568
+ connection_id uuid REFERENCES analytics_connections(id) ON DELETE SET NULL,
1569
+ outcome_family text NOT NULL,
1570
+ authority_role text NOT NULL CHECK (authority_role IN ('primary','observation')),
1571
+ state text NOT NULL CHECK (state IN ('configured_unverified','verified','live','degraded','disabled','blocked')),
1572
+ current_revision integer NOT NULL DEFAULT 1 CHECK (current_revision > 0),
1573
+ created_by_user_id bigint NOT NULL,
1574
+ created_at timestamptz NOT NULL DEFAULT now(),
1575
+ updated_at timestamptz NOT NULL DEFAULT now(),
1576
+ disabled_at timestamptz,
1577
+ UNIQUE(site_id, provider, source_account_ref, outcome_family)
1578
+ )
1579
+ `),await t.query(`CREATE UNIQUE INDEX IF NOT EXISTS analytics_revenue_authorities_one_primary
1580
+ ON analytics_revenue_authorities(site_id, outcome_family)
1581
+ WHERE authority_role='primary' AND state <> 'disabled'`),await t.query(`
1582
+ CREATE TABLE IF NOT EXISTS analytics_provider_customer_bindings (
1583
+ binding_id uuid PRIMARY KEY,
1584
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1585
+ authority_id uuid NOT NULL REFERENCES analytics_revenue_authorities(authority_id) ON DELETE CASCADE,
1586
+ provider_customer_ref_hmac text NOT NULL CHECK (provider_customer_ref_hmac ~ '^[a-f0-9]{64}$'),
1587
+ person_id uuid NOT NULL REFERENCES analytics_people(id) ON DELETE CASCADE,
1588
+ verification_status text NOT NULL CHECK (verification_status IN ('verified','revoked')),
1589
+ evidence_ref text NOT NULL,
1590
+ created_at timestamptz NOT NULL DEFAULT now(),
1591
+ updated_at timestamptz NOT NULL DEFAULT now(),
1592
+ revoked_at timestamptz,
1593
+ UNIQUE(authority_id, provider_customer_ref_hmac),
1594
+ UNIQUE(authority_id, person_id)
1595
+ )
1596
+ `),await t.query(`
1597
+ CREATE TABLE IF NOT EXISTS analytics_authoritative_outcomes (
1598
+ outcome_id uuid PRIMARY KEY,
1599
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1600
+ authority_id uuid NOT NULL REFERENCES analytics_revenue_authorities(authority_id) ON DELETE RESTRICT,
1601
+ authority_key text NOT NULL,
1602
+ provider_object_kind text NOT NULL,
1603
+ provider_object_ref_hmac text NOT NULL,
1604
+ provider_customer_ref_hmac text,
1605
+ outcome_kind text NOT NULL,
1606
+ current_version integer NOT NULL DEFAULT 0 CHECK (current_version >= 0),
1607
+ conversion_id uuid REFERENCES analytics_conversions(id) ON DELETE SET NULL,
1608
+ person_id uuid REFERENCES analytics_people(id) ON DELETE SET NULL,
1609
+ first_observed_at timestamptz NOT NULL,
1610
+ last_observed_at timestamptz NOT NULL,
1611
+ created_at timestamptz NOT NULL DEFAULT now(),
1612
+ UNIQUE(site_id, authority_key)
1613
+ )
1614
+ `),await t.query(`
1615
+ CREATE TABLE IF NOT EXISTS analytics_authoritative_outcome_versions (
1616
+ version_id uuid NOT NULL UNIQUE,
1617
+ outcome_id uuid NOT NULL REFERENCES analytics_authoritative_outcomes(outcome_id) ON DELETE CASCADE,
1618
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1619
+ version integer NOT NULL CHECK (version > 0),
1620
+ transport_event_id text NOT NULL,
1621
+ status text NOT NULL,
1622
+ value_minor bigint NOT NULL,
1623
+ currency text NOT NULL CHECK (currency ~ '^[A-Z]{3}$'),
1624
+ occurred_at timestamptz NOT NULL,
1625
+ observed_at timestamptz NOT NULL,
1626
+ provenance jsonb NOT NULL DEFAULT '{}'::jsonb,
1627
+ receipt_ref text NOT NULL,
1628
+ request_fingerprint text NOT NULL CHECK (request_fingerprint ~ '^[a-f0-9]{64}$'),
1629
+ created_at timestamptz NOT NULL DEFAULT now(),
1630
+ PRIMARY KEY(outcome_id, version),
1631
+ UNIQUE(site_id, transport_event_id)
1632
+ )
1633
+ `),await t.query(`
1634
+ CREATE TABLE IF NOT EXISTS analytics_revenue_setup_revisions (
1635
+ setup_id uuid NOT NULL,
1636
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1637
+ revision integer NOT NULL CHECK (revision > 0),
1638
+ authority_id uuid REFERENCES analytics_revenue_authorities(authority_id) ON DELETE SET NULL,
1639
+ state text NOT NULL,
1640
+ plan jsonb NOT NULL,
1641
+ request_fingerprint text NOT NULL CHECK (request_fingerprint ~ '^[a-f0-9]{64}$'),
1642
+ created_by_user_id bigint NOT NULL,
1643
+ created_at timestamptz NOT NULL DEFAULT now(),
1644
+ PRIMARY KEY(setup_id, revision)
1645
+ )
1646
+ `),await t.query(`
1647
+ CREATE TABLE IF NOT EXISTS analytics_revenue_setup_receipts (
1648
+ receipt_id uuid PRIMARY KEY,
1649
+ setup_id uuid NOT NULL,
1650
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1651
+ revision integer NOT NULL,
1652
+ operation text NOT NULL,
1653
+ status text NOT NULL,
1654
+ idempotency_key text NOT NULL,
1655
+ request_fingerprint text NOT NULL CHECK (request_fingerprint ~ '^[a-f0-9]{64}$'),
1656
+ receipt jsonb NOT NULL,
1657
+ occurred_at timestamptz NOT NULL DEFAULT now(),
1658
+ FOREIGN KEY(setup_id, revision) REFERENCES analytics_revenue_setup_revisions(setup_id, revision) ON DELETE CASCADE,
1659
+ UNIQUE(site_id, idempotency_key)
1660
+ )
1661
+ `),await t.query(`
1662
+ CREATE TABLE IF NOT EXISTS analytics_post_purchase_surveys (
1663
+ survey_id uuid PRIMARY KEY,
1664
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1665
+ current_revision integer NOT NULL DEFAULT 1 CHECK (current_revision > 0),
1666
+ state text NOT NULL CHECK (state IN ('draft','published','paused','archived')),
1667
+ name text NOT NULL,
1668
+ question text NOT NULL,
1669
+ selection_mode text NOT NULL CHECK (selection_mode IN ('single','multiple')),
1670
+ eligible_outcome_families text[] NOT NULL,
1671
+ exchange_ttl_seconds integer NOT NULL CHECK (exchange_ttl_seconds BETWEEN 60 AND 86400),
1672
+ session_ttl_seconds integer NOT NULL CHECK (session_ttl_seconds BETWEEN 60 AND 86400),
1673
+ approved_copy_id text,
1674
+ create_idempotency_key text,
1675
+ create_request_fingerprint text,
1676
+ state_transition_receipts jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(state_transition_receipts)='object'),
1677
+ created_by_user_id bigint NOT NULL,
1678
+ created_at timestamptz NOT NULL DEFAULT now(),
1679
+ updated_at timestamptz NOT NULL DEFAULT now(),
1680
+ archived_at timestamptz,
1681
+ UNIQUE(site_id, create_idempotency_key)
1682
+ )
1683
+ `),await t.query(`ALTER TABLE analytics_post_purchase_surveys
1684
+ ADD COLUMN IF NOT EXISTS state_transition_receipts jsonb NOT NULL DEFAULT '{}'::jsonb`),await t.query(`ALTER TABLE analytics_post_purchase_surveys
1685
+ DROP CONSTRAINT IF EXISTS analytics_post_purchase_surveys_state_transition_receipts_check`),await t.query(`ALTER TABLE analytics_post_purchase_surveys
1686
+ ADD CONSTRAINT analytics_post_purchase_surveys_state_transition_receipts_check
1687
+ CHECK (jsonb_typeof(state_transition_receipts)='object')`),await t.query(`
1688
+ CREATE TABLE IF NOT EXISTS analytics_post_purchase_survey_options (
1689
+ survey_id uuid NOT NULL REFERENCES analytics_post_purchase_surveys(survey_id) ON DELETE CASCADE,
1690
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1691
+ revision integer NOT NULL CHECK (revision > 0),
1692
+ option_id text NOT NULL,
1693
+ label text NOT NULL,
1694
+ position integer NOT NULL CHECK (position >= 0),
1695
+ active boolean NOT NULL DEFAULT true,
1696
+ created_at timestamptz NOT NULL DEFAULT now(),
1697
+ PRIMARY KEY(survey_id, revision, option_id),
1698
+ UNIQUE(survey_id, revision, position)
1699
+ )
1700
+ `),await t.query(`
1701
+ CREATE TABLE IF NOT EXISTS analytics_post_purchase_survey_invites (
1702
+ invite_id uuid PRIMARY KEY,
1703
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1704
+ survey_id uuid NOT NULL REFERENCES analytics_post_purchase_surveys(survey_id) ON DELETE CASCADE,
1705
+ survey_revision integer NOT NULL CHECK (survey_revision > 0),
1706
+ conversion_id uuid NOT NULL REFERENCES analytics_conversions(id) ON DELETE CASCADE,
1707
+ invitation_token_digest text NOT NULL CHECK (invitation_token_digest ~ '^[a-f0-9]{64}$'),
1708
+ purpose text NOT NULL DEFAULT 'customer_reported_measurement' CHECK (purpose='customer_reported_measurement'),
1709
+ approved_copy_id text NOT NULL,
1710
+ exchange_expires_at timestamptz NOT NULL,
1711
+ exchanged_at timestamptz,
1712
+ session_token_digest text CHECK (session_token_digest IS NULL OR session_token_digest ~ '^[a-f0-9]{64}$'),
1713
+ session_expires_at timestamptz,
1714
+ response_id uuid,
1715
+ created_at timestamptz NOT NULL DEFAULT now(),
1716
+ UNIQUE(site_id, invitation_token_digest),
1717
+ UNIQUE(site_id, session_token_digest),
1718
+ UNIQUE(survey_id, conversion_id)
1719
+ )
1720
+ `),await t.query(`
1721
+ CREATE TABLE IF NOT EXISTS analytics_post_purchase_survey_responses (
1722
+ response_id uuid PRIMARY KEY,
1723
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1724
+ survey_id uuid NOT NULL REFERENCES analytics_post_purchase_surveys(survey_id) ON DELETE CASCADE,
1725
+ survey_revision integer NOT NULL CHECK (survey_revision > 0),
1726
+ conversion_id uuid NOT NULL REFERENCES analytics_conversions(id) ON DELETE CASCADE,
1727
+ invite_id uuid NOT NULL REFERENCES analytics_post_purchase_survey_invites(invite_id) ON DELETE RESTRICT,
1728
+ body_fingerprint text NOT NULL CHECK (body_fingerprint ~ '^[a-f0-9]{64}$'),
1729
+ submitted_at timestamptz NOT NULL DEFAULT now(),
1730
+ UNIQUE(survey_id, conversion_id),
1731
+ UNIQUE(invite_id)
1732
+ )
1733
+ `),await t.query(`
1734
+ CREATE TABLE IF NOT EXISTS analytics_reported_influences (
1735
+ evidence_id uuid PRIMARY KEY,
1736
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1737
+ response_id uuid NOT NULL REFERENCES analytics_post_purchase_survey_responses(response_id) ON DELETE CASCADE,
1738
+ conversion_id uuid NOT NULL REFERENCES analytics_conversions(id) ON DELETE CASCADE,
1739
+ survey_id uuid NOT NULL REFERENCES analytics_post_purchase_surveys(survey_id) ON DELETE CASCADE,
1740
+ survey_revision integer NOT NULL CHECK (survey_revision > 0),
1741
+ option_id text NOT NULL,
1742
+ option_label text NOT NULL,
1743
+ created_at timestamptz NOT NULL DEFAULT now(),
1744
+ UNIQUE(response_id, option_id)
1745
+ )
1746
+ `),await t.query(`
1747
+ CREATE TABLE IF NOT EXISTS analytics_view_evidence_receipts (
1748
+ receipt_id uuid PRIMARY KEY,
1749
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1750
+ provider text NOT NULL,
1751
+ source_account_ref text NOT NULL,
1752
+ provider_event_id text NOT NULL,
1753
+ evidence_granularity text NOT NULL CHECK (evidence_granularity='event_level'),
1754
+ verification_status text NOT NULL CHECK (verification_status='verified'),
1755
+ subject_kind text NOT NULL CHECK (subject_kind IN ('person','session','visitor')),
1756
+ subject_ref_hmac text NOT NULL,
1757
+ person_id uuid REFERENCES analytics_people(id) ON DELETE SET NULL,
1758
+ session_id uuid REFERENCES analytics_sessions(id) ON DELETE SET NULL,
1759
+ provider_receipt_ref text NOT NULL,
1760
+ occurred_at timestamptz NOT NULL,
1761
+ retention_expires_at timestamptz NOT NULL,
1762
+ touch_id uuid REFERENCES analytics_attribution_touches(id) ON DELETE SET NULL,
1763
+ created_at timestamptz NOT NULL DEFAULT now(),
1764
+ UNIQUE(site_id, provider, source_account_ref, provider_event_id)
1765
+ )
1766
+ `),await t.query("ALTER TABLE analytics_conversions DROP CONSTRAINT IF EXISTS analytics_conversions_authority_id_fkey"),await t.query(`ALTER TABLE analytics_conversions ADD CONSTRAINT analytics_conversions_authority_id_fkey
1767
+ FOREIGN KEY(authority_id) REFERENCES analytics_revenue_authorities(authority_id) ON DELETE SET NULL`),await t.query("ALTER TABLE analytics_conversions DROP CONSTRAINT IF EXISTS analytics_conversions_authority_version_id_fkey"),await t.query(`ALTER TABLE analytics_conversions ADD CONSTRAINT analytics_conversions_authority_version_id_fkey
1768
+ FOREIGN KEY(authority_version_id) REFERENCES analytics_authoritative_outcome_versions(version_id) ON DELETE SET NULL`),await t.query(`CREATE INDEX IF NOT EXISTS analytics_authoritative_outcomes_person
1769
+ ON analytics_authoritative_outcomes(site_id, person_id, last_observed_at DESC)`),await t.query(`CREATE INDEX IF NOT EXISTS analytics_reported_influences_conversion
1770
+ ON analytics_reported_influences(site_id, conversion_id, created_at)`),await t.query(`CREATE INDEX IF NOT EXISTS analytics_view_evidence_subject
1771
+ ON analytics_view_evidence_receipts(site_id, subject_ref_hmac, occurred_at DESC)`),await t.query(`
1772
+ INSERT INTO analytics_schema_migrations(version)
1773
+ VALUES ('2026-08-05.1'), ('2026-08-05.2'), ('2026-08-05.3'), ('2026-08-05.4'), ('2026-08-05.5'), ('2026-08-05.6'), ('2026-08-05.7'), ('2026-08-05.8'), ('2026-08-05.9'), ('2026-08-05.10'), ('2026-08-05.11'), ('2026-08-05.12'), ('2026-08-05.13'), ('2026-08-05.14'), ('2026-08-26.1'), ('2026-08-26.2'), ('2026-08-26.3'), ('2026-08-26.4'), ('2026-08-27.1'), ('2026-08-28.1'), ('2026-08-28.2'), ('2026-08-28.3'), ('2026-08-28.4'), ('2026-08-31.1'), ('2026-09-01.1'), ('2026-09-01.2')
1774
+ ON CONFLICT (version) DO NOTHING
1775
+ `)}catch(i){throw i}finally{t.release()}}function Qe(e){return e.trim().toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,80)||`site-${P(4).toString("hex")}`}function G(e){if(!e||e==="null")return null;try{let t=new URL(e);return t.protocol!=="http:"&&t.protocol!=="https:"?null:t.hostname.toLowerCase().replace(/\.$/,"")}catch{return null}}function Vt(){return`px_${P(18).toString("base64url")}`}async function g(e,t,i){let s=(await e.query("SELECT role FROM analytics_site_grants WHERE site_id = $1 AND user_id = $2",[t,i])).rows[0]?.role;if(!s)throw new m("analytics_site_not_found","Site not found.",404);return s}async function D(e,t,i){let n=await g(e,t,i);if(n==="viewer")throw new m("analytics_forbidden","Editor access is required.",403);return n}async function Qi(e,t,i){return g(e,t,i)}async function Zi(e,t,i){return D(e,t,i)}async function en(e){let i=await l().connect(),n=y(),s=Qe(e.slug||e.name);try{await i.query("BEGIN");let a=s;for(let r=0;r<3;r+=1)try{await i.query(`INSERT INTO analytics_sites(id, slug, name, owner_user_id, timezone)
1776
+ VALUES ($1, $2, $3, $4, $5)`,[n,a,e.name.trim(),e.userId,e.timezone||"UTC"]);break}catch(o){if(o.code!=="23505"||r===2)throw o;a=`${s}-${P(2).toString("hex")}`}return await i.query("INSERT INTO analytics_site_grants(site_id, user_id, role) VALUES ($1, $2, 'owner')",[n,e.userId]),await i.query("COMMIT"),{id:n,slug:a,name:e.name.trim(),timezone:e.timezone||"UTC",status:"active",role:"owner",pixel_count:0,last_event_at:null,business_model:"leadgen"}}catch(a){throw await i.query("ROLLBACK"),a}finally{i.release()}}async function Jt(e){let t=await l().query(`SELECT s.id, s.slug, s.name, s.timezone, s.status, s.business_model, g.role,
1777
+ count(DISTINCT p.id)::int AS pixel_count,
1778
+ max(e.occurred_at)::text AS last_event_at
1779
+ FROM analytics_sites s
1780
+ JOIN analytics_site_grants g ON g.site_id = s.id AND g.user_id = $1
1781
+ LEFT JOIN analytics_pixels p ON p.site_id = s.id
1782
+ LEFT JOIN analytics_events e ON e.site_id = s.id
1783
+ GROUP BY s.id, g.role
1784
+ ORDER BY s.created_at ASC`,[e]);return t.rows}async function tn(e){let t=l();await D(t,e.siteId,e.userId),await t.query("UPDATE analytics_sites SET business_model = $1, updated_at = now() WHERE id = $2",[e.businessModel,e.siteId])}async function nn(e){let t=l();await D(t,e.siteId,e.userId);for(let i of e.rows)await t.query(`INSERT INTO analytics_ad_spend_daily(site_id, bucket, platform, campaign, spend_minor, currency, source_ref)
1785
+ VALUES ($1,$2::date,$3,$4,$5,$6,$7)
1786
+ ON CONFLICT(site_id,bucket,platform,campaign) DO UPDATE SET spend_minor=EXCLUDED.spend_minor,currency=EXCLUDED.currency,source_ref=EXCLUDED.source_ref,updated_at=now()`,[e.siteId,i.date,i.platform.slice(0,80),i.campaign?.slice(0,240)||"(all)",i.spendMinor,(i.currency||"USD").toUpperCase(),i.sourceRef?.slice(0,500)||null]);return{accepted:e.rows.length}}async function sn(e,t,i,n){let s=l();await g(s,e,t);let a=await s.query("SELECT business_model FROM analytics_sites WHERE id=$1",[e]),r=await s.query(`SELECT count(*)::int AS conversions, count(DISTINCT person_id)::int AS customers,
1787
+ count(*) FILTER (WHERE conversion_kind IN ('form_submitted','lead','qualified_lead'))::int AS leads,
1788
+ count(*) FILTER (WHERE conversion_kind IN ('purchase','order_completed'))::int AS purchases,
1789
+ count(*) FILTER (WHERE conversion_kind IN ('trial_started','demo_started'))::int AS trials,
1790
+ count(*) FILTER (WHERE conversion_kind IN ('subscription_started','subscription_renewed'))::int AS subscriptions,
1791
+ count(*) FILTER (WHERE conversion_kind IN ('subscription_cancelled','churned'))::int AS churns,
1792
+ COALESCE(sum(value_minor),0)::bigint AS revenue_minor,
1793
+ (SELECT count(*)::int FROM (SELECT person_id FROM analytics_conversions WHERE site_id=$1 AND person_id IS NOT NULL AND conversion_kind IN ('purchase','order_completed') AND occurred_at >= $2 AND occurred_at < $3 GROUP BY person_id HAVING count(*) > 1) r) AS repeat_customers
1794
+ FROM analytics_conversions WHERE site_id=$1 AND occurred_at >= $2 AND occurred_at < $3`,[e,i,n]),o=await s.query("SELECT COALESCE(sum(spend_minor),0)::bigint AS spend_minor FROM analytics_ad_spend_daily WHERE site_id=$1 AND bucket >= $2::date AND bucket < $3::date",[e,i,n]),d=r.rows[0],_=Number(o.rows[0]?.spend_minor||0),u=Number(d.revenue_minor||0);return{businessModel:a.rows[0]?.business_model||"leadgen",period:{start:i,end:n},metrics:{...d,revenue_minor:u,spend_minor:_,roas:_>0?u/_:null,cost_per_lead:_>0&&d.leads>0?_/d.leads:null,average_order_value:d.purchases>0?u/d.purchases:null,repeat_customer_rate:d.customers>0?d.repeat_customers/d.customers:null,churn_rate:d.subscriptions>0?d.churns/d.subscriptions:null},missingInputs:_>0?[]:["ad_spend"]}}async function an(e,t,i=12){let n=l();await g(n,e,t);let a=(await n.query(`WITH months AS (SELECT generate_series(date_trunc('month', now()) - interval '11 months', date_trunc('month', now()), interval '1 month') AS month)
1795
+ SELECT m.month::date::text AS month,
1796
+ COALESCE((SELECT sum(value_minor) FROM analytics_conversions c WHERE c.site_id=$1 AND c.occurred_at >= m.month AND c.occurred_at < m.month+interval '1 month'),0)::bigint AS revenue_minor,
1797
+ COALESCE((SELECT sum(spend_minor) FROM analytics_ad_spend_daily s WHERE s.site_id=$1 AND s.bucket >= m.month::date AND s.bucket < (m.month+interval '1 month')::date),0)::bigint AS spend_minor
1798
+ FROM months m ORDER BY m.month`,[e])).rows.map(T=>({month:T.month,revenueMinor:Number(T.revenue_minor),spendMinor:Number(T.spend_minor)}));if(a.filter(T=>T.revenueMinor>0||T.spendMinor>0).length<3)return{status:"insufficient_data",history:a,forecast:[],horizonMonths:i,missingInputs:["at_least_3_months_of_revenue_or_spend"]};let o=a.map(T=>T.revenueMinor),d=o.length,_=(d-1)/2,u=o.reduce((T,O)=>T+O,0)/d,c=o.reduce((T,O,w)=>T+(w-_)*(O-u),0)/o.reduce((T,O,w)=>T+(w-_)**2,0),E=u-c*_,N=Math.sqrt(o.reduce((T,O,w)=>T+(O-(E+c*w))**2,0)/d),A=a.filter(T=>T.spendMinor>0),v=A.length?A.reduce((T,O)=>T+O.spendMinor,0)/A.length:0,R=new Date(`${a.at(-1)?.month}T00:00:00Z`),C=Array.from({length:Math.min(Math.max(i,1),36)},(T,O)=>{let w=new Date(Date.UTC(R.getUTCFullYear(),R.getUTCMonth()+O+1,1)),k=Math.max(0,E+c*(d+O));return{month:w.toISOString().slice(0,10),lowerRevenueMinor:Math.max(0,Math.round(k-N)),baseRevenueMinor:Math.round(k),upperRevenueMinor:Math.round(k+N),projectedSpendMinor:Math.round(v),baseRoas:v>0?k/v:null}});return{status:"modeled",method:"monthly_linear_trend_v1",history:a,forecast:C,horizonMonths:C.length,missingInputs:v>0?[]:["ad_spend"],caveats:["Scenario range reflects historical residual variation, not a guarantee."]}}async function rn(e){let t=l();if(await g(t,e.siteId,e.userId)!=="owner")throw new m("analytics_forbidden","Owner access is required.",403);if(!(await t.query("DELETE FROM analytics_sites WHERE id = $1 AND owner_user_id = $2",[e.siteId,e.userId])).rowCount)throw new m("analytics_site_not_found","Site not found.",404)}async function on(e){let t=l();return await D(t,e.siteId,e.userId),{...(await t.query(`INSERT INTO analytics_pixels(id, site_id, public_id, name, environment)
1799
+ VALUES ($1, $2, $3, $4, $5)
1800
+ RETURNING id, site_id, public_id, name, environment, status, created_at::text, NULL::text AS last_event_at`,[y(),e.siteId,Vt(),e.name.trim(),e.environment||"production"])).rows[0],domains:[]}}async function Qt(e,t){let i=l();await g(i,e,t);let[n,s]=await Promise.all([i.query(`SELECT p.id, p.site_id, p.public_id, p.name, p.environment, p.status,
1801
+ p.created_at::text, max(e.occurred_at)::text AS last_event_at
1802
+ FROM analytics_pixels p
1803
+ LEFT JOIN analytics_events e ON e.pixel_id = p.id
1804
+ WHERE p.site_id = $1
1805
+ GROUP BY p.id
1806
+ ORDER BY p.created_at ASC`,[e]),i.query(`SELECT pixel_id, hostname, state, first_seen_at::text, last_seen_at::text,
1807
+ accepted_count::int, rejected_count::int
1808
+ FROM analytics_pixel_domains
1809
+ WHERE pixel_id IN (SELECT id FROM analytics_pixels WHERE site_id = $1)
1810
+ ORDER BY last_seen_at DESC`,[e])]);return n.rows.map(a=>({...a,domains:s.rows.filter(r=>r.pixel_id===a.id).map(({pixel_id:r,...o})=>o)}))}async function dn(e){let t=l();if(await D(t,e.siteId,e.userId),!(await t.query(`UPDATE analytics_pixels
1811
+ SET name = COALESCE($3, name),
1812
+ environment = COALESCE($4, environment),
1813
+ status = COALESCE($5, status),
1814
+ disabled_at = CASE WHEN $5 = 'disabled' THEN now() WHEN $5 = 'active' THEN NULL ELSE disabled_at END,
1815
+ updated_at = now()
1816
+ WHERE id = $1 AND site_id = $2`,[e.pixelId,e.siteId,e.name??null,e.environment??null,e.status??null])).rowCount)throw new m("analytics_pixel_not_found","Pixel not found.",404)}async function cn(e){let t=l();await D(t,e.siteId,e.userId);let i=G(`https://${e.hostname}`);if(!i)throw new m("invalid_hostname","Enter a valid hostname.");if(!(await t.query(`INSERT INTO analytics_pixel_domains(id, pixel_id, hostname, state)
1817
+ SELECT $1, p.id, $4, $5 FROM analytics_pixels p WHERE p.id = $2 AND p.site_id = $3
1818
+ ON CONFLICT(pixel_id, hostname) DO UPDATE SET state = EXCLUDED.state, updated_at = now()
1819
+ RETURNING id`,[y(),e.pixelId,e.siteId,i,e.state])).rowCount)throw new m("analytics_pixel_not_found","Pixel not found.",404)}async function _n(e){let t=await l().connect();try{await t.query("BEGIN"),await D(t,e.siteId,e.userId);let i=[...new Set(e.hostnames.map(a=>G(`https://${a}`)).filter(a=>!!a))];if(i.length<2||i.length>20)throw new m("invalid_host_group","A host group requires between two and twenty valid hostnames.");if((await t.query(`SELECT d.hostname
1820
+ FROM analytics_pixels p
1821
+ JOIN analytics_pixel_domains d ON d.pixel_id=p.id
1822
+ WHERE p.id=$1 AND p.site_id=$2 AND p.status='active'
1823
+ AND d.state='approved' AND d.hostname = ANY($3::text[])`,[e.pixelId,e.siteId,i])).rows.length!==i.length)throw new m("host_group_unapproved","Every host group member must be approved for this active Pixel.",403);let s=await t.query(`INSERT INTO analytics_host_groups(id, site_id, pixel_id, name, auto_decorate)
1824
+ VALUES ($1,$2,$3,$4,$5)
1825
+ ON CONFLICT(pixel_id, name) DO UPDATE SET
1826
+ auto_decorate=EXCLUDED.auto_decorate, updated_at=now()
1827
+ RETURNING id`,[y(),e.siteId,e.pixelId,e.name.trim().slice(0,120),!!e.autoDecorate]);await t.query("DELETE FROM analytics_host_group_members WHERE group_id=$1",[s.rows[0].id]);for(let a of i)await t.query("INSERT INTO analytics_host_group_members(group_id, hostname) VALUES ($1,$2)",[s.rows[0].id,a]);return await t.query("COMMIT"),{id:s.rows[0].id,siteId:e.siteId,pixelId:e.pixelId,name:e.name.trim().slice(0,120),autoDecorate:!!e.autoDecorate,hostnames:i}}catch(i){throw await t.query("ROLLBACK"),i}finally{t.release()}}async function un(e){let t=l();return await g(t,e.siteId,e.userId),(await t.query(`SELECT g.id, g.site_id, g.pixel_id, g.name, g.auto_decorate,
1828
+ COALESCE(array_agg(m.hostname ORDER BY m.hostname) FILTER (WHERE m.hostname IS NOT NULL), '{}') AS hostnames
1829
+ FROM analytics_host_groups g
1830
+ LEFT JOIN analytics_host_group_members m ON m.group_id=g.id
1831
+ WHERE g.site_id=$1 AND g.pixel_id=$2
1832
+ GROUP BY g.id ORDER BY g.created_at`,[e.siteId,e.pixelId])).rows.map(n=>({id:n.id,siteId:n.site_id,pixelId:n.pixel_id,name:n.name,autoDecorate:n.auto_decorate,hostnames:n.hostnames}))}async function En(e){let t=await l().connect(),i=G(`https://${e.sourceHostname}`),n=G(`https://${e.audienceHostname}`);if(!i||!n||i===n)throw new m("linker_host_invalid","Choose a different approved destination hostname.",403);try{await t.query("BEGIN");let s=await t.query(`SELECT p.site_id, p.id AS pixel_id, g.id AS host_group_id
1833
+ FROM analytics_pixels p
1834
+ JOIN analytics_host_groups g ON g.pixel_id=p.id AND g.site_id=p.site_id
1835
+ JOIN analytics_host_group_members source ON source.group_id=g.id AND source.hostname=$2
1836
+ JOIN analytics_host_group_members audience ON audience.group_id=g.id AND audience.hostname=$3
1837
+ JOIN analytics_pixel_domains source_domain ON source_domain.pixel_id=p.id AND source_domain.hostname=$2 AND source_domain.state='approved'
1838
+ JOIN analytics_pixel_domains audience_domain ON audience_domain.pixel_id=p.id AND audience_domain.hostname=$3 AND audience_domain.state='approved'
1839
+ WHERE p.public_id=$1 AND p.status='active'
1840
+ AND NOT EXISTS (
1841
+ SELECT 1 FROM analytics_host_group_members member
1842
+ LEFT JOIN analytics_pixel_domains member_domain
1843
+ ON member_domain.pixel_id=p.id AND member_domain.hostname=member.hostname
1844
+ WHERE member.group_id=g.id AND COALESCE(member_domain.state, '') <> 'approved'
1845
+ )
1846
+ LIMIT 1`,[e.publicPixelId,i,n]);if(!s.rowCount)throw new m("linker_host_unapproved","Both hostnames must belong to the same approved Pixel host group.",403);let{site_id:a,pixel_id:r,host_group_id:o}=s.rows[0],d=await t.query(`INSERT INTO analytics_identity_nodes(id, site_id, kind, value_hmac)
1847
+ VALUES ($1,$2,'visitor_id',$3)
1848
+ ON CONFLICT(site_id, kind, value_hmac) DO UPDATE SET last_seen_at=now()
1849
+ RETURNING id`,[y(),a,L("visitor_id",e.visitorId)]),u=(await t.query(`SELECT person_id FROM analytics_identity_assignments
1850
+ WHERE site_id=$1 AND identity_node_id=$2`,[a,d.rows[0].id])).rows[0]?.person_id;u||(u=(await t.query(`INSERT INTO analytics_people(id, site_id, crm_person_ref, public_ref) VALUES ($1,$2,$3,$4)
1851
+ ON CONFLICT(site_id, crm_person_ref) DO UPDATE SET last_seen_at=now(),
1852
+ public_ref=COALESCE(analytics_people.public_ref,EXCLUDED.public_ref) RETURNING id`,[y(),a,`XRay::anonymous::${L("visitor_id",e.visitorId).slice(0,24)}`,ye()])).rows[0].id,await t.query(`INSERT INTO analytics_identity_edges(id, site_id, person_id, identity_node_id, evidence_kind, confidence)
1853
+ VALUES ($1,$2,$3,$4,'first_party_linker',0.980)
1854
+ ON CONFLICT(person_id, identity_node_id, evidence_kind) DO UPDATE SET last_seen_at=now()`,[y(),a,u,d.rows[0].id]),await J(t,{siteId:a,personId:u,identityNodeId:d.rows[0].id,evidenceKind:"first_party_linker"}));let c=y();return await t.query(`INSERT INTO analytics_linker_tokens(
1855
+ id, site_id, pixel_id, host_group_id, identity_node_id,
1856
+ source_hostname, audience_hostname, expires_at
1857
+ ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8)`,[c,a,r,o,d.rows[0].id,i,n,e.expiresAt]),await t.query("COMMIT"),{tokenId:c,siteId:a,pixelId:r,identityNodeId:d.rows[0].id,sourceHostname:i,audienceHostname:n,expiresAt:e.expiresAt}}catch(s){throw await t.query("ROLLBACK"),s}finally{t.release()}}async function ln(e){let t=await l().connect();try{if(await t.query("BEGIN"),!(await t.query(`UPDATE analytics_linker_tokens SET consumed_at=now()
1858
+ WHERE id=$1 AND site_id=$2 AND pixel_id=$3 AND identity_node_id=$4
1859
+ AND audience_hostname=$5 AND consumed_at IS NULL AND expires_at > now()
1860
+ RETURNING identity_node_id`,[e.tokenId,e.siteId,e.pixelId,e.identityNodeId,e.audienceHostname])).rowCount)throw new m("linker_replayed_or_expired","The linker token was already used or has expired.",409);let s=(await t.query(`SELECT person_id FROM analytics_identity_assignments
1861
+ WHERE site_id=$1 AND identity_node_id=$2`,[e.siteId,e.identityNodeId])).rows[0]?.person_id;if(!s)throw new m("linker_identity_missing","The linker identity is unavailable.",409);let a=await t.query(`INSERT INTO analytics_identity_nodes(id, site_id, kind, value_hmac)
1862
+ VALUES ($1,$2,'visitor_id',$3)
1863
+ ON CONFLICT(site_id, kind, value_hmac) DO UPDATE SET last_seen_at=now()
1864
+ RETURNING id`,[y(),e.siteId,L("visitor_id",e.visitorId)]);if(await t.query(`INSERT INTO analytics_identity_edges(id, site_id, person_id, identity_node_id, evidence_kind, confidence)
1865
+ VALUES ($1,$2,$3,$4,'cross_domain_linker',0.980)
1866
+ ON CONFLICT(person_id, identity_node_id, evidence_kind) DO UPDATE SET last_seen_at=now()`,[y(),e.siteId,s,a.rows[0].id]),await J(t,{siteId:e.siteId,personId:s,identityNodeId:a.rows[0].id,evidenceKind:"cross_domain_linker"})==="conflict")throw new m("identity_assignment_conflict","The destination identity is already assigned and requires review.",409);await t.query("UPDATE analytics_events SET person_id=$1 WHERE site_id=$2 AND visitor_id=$3 AND person_id IS NULL",[s,e.siteId,e.visitorId]);let o=L("visitor_id",e.visitorId);return await t.query("UPDATE analytics_sessions SET person_id=$1 WHERE site_id=$2 AND visitor_hmac=$3 AND person_id IS NULL",[s,e.siteId,o]),await t.query("UPDATE analytics_attribution_touches SET person_id=$1 WHERE site_id=$2 AND visitor_hmac=$3 AND person_id IS NULL",[s,e.siteId,o]),await t.query("COMMIT"),{personId:s}}catch(i){throw await t.query("ROLLBACK"),i}finally{t.release()}}function Ze(e){return e.consent?.analytics==="granted"&&(e.consent.purposes?.enhanced_matching==="granted"||e.consent.adUserData==="granted")&&e.consent.globalPrivacyControl!==!0}function Zt(e){return!!(e.identity&&Ze(e))}async function ei(e,t){let i=t.event.visitorId?.trim().slice(0,240),n=t.event.consent;if(!i||!n)return null;let s=Date.parse(t.event.occurredAt),a=Date.now();if(!Number.isFinite(s)||s>a+5*6e4||s<a-1440*6e4)return null;let r=n.purposes,o={siteAnalytics:r?r.site_analytics==="granted":n.analytics==="granted",enhancedMatching:r?r.enhanced_matching==="granted":n.adUserData==="granted",advertisingMeasurement:r?r.advertising_measurement==="granted":n.adPersonalization==="granted"},d=n.globalPrivacyControl===!0;d&&(o.siteAnalytics=!1,o.enhancedMatching=!1,o.advertisingMeasurement=!1);let _=d?"gpc":Object.values(o).every(Boolean)?"accept_all":Object.values(o).every(A=>!A)?"reject_all":"save_choices",u=new Date(s).toISOString(),E=(await e.query(`SELECT s.latest_receipt_id::text,s.site_analytics,s.enhanced_matching,s.advertising_measurement,s.gpc,r.action
1867
+ FROM analytics_consent_current_state s JOIN analytics_consent_receipts r ON r.id=s.latest_receipt_id
1868
+ WHERE s.site_id=$1 AND s.subject_ref=$2 FOR UPDATE`,[t.siteId,i])).rows[0];if(E&&E.site_analytics===o.siteAnalytics&&E.enhanced_matching===o.enhancedMatching&&E.advertising_measurement===o.advertisingMeasurement&&E.gpc===d&&E.action===_)return E.latest_receipt_id;let N=y();return await e.query(`INSERT INTO analytics_consent_receipts(id,site_id,pixel_id,subject_ref,policy_version,jurisdiction_profile,
1869
+ necessary,site_analytics,enhanced_matching,advertising_measurement,gpc,action,source,presented_at,occurred_at,supersedes_id)
1870
+ VALUES($1,$2,$3,$4,1,'pixel',true,$5,$6,$7,$8,$9,'gateway',$10,$10,$11)`,[N,t.siteId,t.pixelId,i,o.siteAnalytics,o.enhancedMatching,o.advertisingMeasurement,d,_,u,E?.latest_receipt_id??null]),await e.query(`INSERT INTO analytics_consent_current_state(site_id,subject_ref,latest_receipt_id,policy_version,jurisdiction_profile,
1871
+ necessary,site_analytics,enhanced_matching,advertising_measurement,gpc,effective_at)
1872
+ VALUES($1,$2,$3,1,'pixel',true,$4,$5,$6,$7,$8)
1873
+ ON CONFLICT(site_id,subject_ref) DO UPDATE SET latest_receipt_id=EXCLUDED.latest_receipt_id,
1874
+ policy_version=EXCLUDED.policy_version,jurisdiction_profile=EXCLUDED.jurisdiction_profile,
1875
+ site_analytics=EXCLUDED.site_analytics,enhanced_matching=EXCLUDED.enhanced_matching,
1876
+ advertising_measurement=EXCLUDED.advertising_measurement,gpc=EXCLUDED.gpc,
1877
+ effective_at=EXCLUDED.effective_at,updated_at=now()
1878
+ WHERE EXCLUDED.effective_at>=analytics_consent_current_state.effective_at`,[t.siteId,i,N,o.siteAnalytics,o.enhancedMatching,o.advertisingMeasurement,d,u]),N}async function Nn(e,t){if(!t.subjectRef)return{personId:null,activationEligible:!1};let n=(await e.query(`SELECT p.id::text AS person_id,s.advertising_measurement
1879
+ FROM analytics_people p
1880
+ JOIN analytics_consent_current_state s ON s.site_id=p.site_id AND s.subject_ref=$2
1881
+ JOIN analytics_consent_receipts r ON r.id=s.latest_receipt_id AND r.site_id=s.site_id
1882
+ WHERE p.site_id=$1
1883
+ AND (($3::uuid IS NOT NULL AND p.id=$3) OR ($4::text IS NOT NULL AND EXISTS (
1884
+ SELECT 1 FROM analytics_events e WHERE e.site_id=p.site_id AND e.event_id=$4
1885
+ AND e.person_id=p.id AND e.visitor_id=$2
1886
+ )))
1887
+ AND s.site_analytics=true AND s.gpc=false AND r.action NOT IN ('withdraw','gpc')
1888
+ AND (s.expires_at IS NULL OR s.expires_at>now()) LIMIT 1`,[t.siteId,t.subjectRef,t.personId??null,t.eventId??null])).rows[0];return n?{personId:n.person_id,activationEligible:n.advertising_measurement===!0}:{personId:null,activationEligible:!1}}async function et(e,t,i,n){return!i?.trim()||!n?.trim()?!1:!!(await e.query(`SELECT 1 FROM analytics_consent_current_state s
1889
+ JOIN analytics_consent_receipts r ON r.id=s.latest_receipt_id AND r.site_id=s.site_id
1890
+ WHERE s.site_id=$1 AND s.subject_ref=$2
1891
+ AND s.site_analytics=true AND s.enhanced_matching=true AND s.gpc=false
1892
+ AND r.action NOT IN ('withdraw','gpc')
1893
+ AND (s.expires_at IS NULL OR s.expires_at>now())
1894
+ AND EXISTS (SELECT 1 FROM analytics_sessions x
1895
+ WHERE x.site_id=s.site_id AND x.visitor_hmac=$3 AND x.session_hmac=$4)
1896
+ ORDER BY s.effective_at DESC LIMIT 1`,[t,i,L("visitor_id",i),L("session_id",n)])).rowCount}async function ti(e,t,i){return!!(await e.query(`SELECT 1 FROM analytics_consent_current_state s
1897
+ JOIN analytics_consent_receipts r ON r.id=s.latest_receipt_id AND r.site_id=s.site_id
1898
+ WHERE s.site_id=$1 AND s.subject_ref=$2
1899
+ AND s.site_analytics=true AND s.enhanced_matching=true AND s.gpc=false
1900
+ AND r.action NOT IN ('withdraw','gpc')
1901
+ AND (s.expires_at IS NULL OR s.expires_at>now()) LIMIT 1`,[t,i])).rowCount}var ii=1800*1e3,ni=1e4;function si(e){return typeof e=="number"&&Number.isFinite(e)&&e>=0?Math.min(Math.trunc(e),ii):null}function ai(e){return typeof e=="number"&&Number.isFinite(e)&&e>=0?Math.min(Math.trunc(e),100):null}var ri=/(email|phone|password|token|secret|authorization|cookie|name|address|form|body)/i;function tt(e){if(!e)return"/";try{return new URL(e,"https://analytics.invalid").pathname.slice(0,2e3)||"/"}catch{return"/"}}function V(e){if(!e)return null;try{let t=new URL(e);return t.protocol!=="http:"&&t.protocol!=="https:"?null:(t.search="",t.hash="",t.toString().slice(0,3e3))}catch{return null}}function oi(e){let t={};for(let[i,n]of Object.entries(e??{}).slice(0,24))!/^[a-z][a-z0-9_]{0,63}$/i.test(i)||ri.test(i)||(n===null||typeof n=="boolean"||typeof n=="number"?t[i]=n:typeof n=="string"&&(t[i]=n.slice(0,300)));return t}async function di(e){if(!e.event.visitorId||!e.event.sessionId)return;let t=L("visitor_id",e.event.visitorId),i=L("session_id",e.event.sessionId),n=new Date(e.event.occurredAt);if(!Number.isFinite(n.getTime()))return;let s=tt(e.event.path||e.event.canonicalUrl),a=await e.client.query(`INSERT INTO analytics_sessions(
1902
+ id, site_id, pixel_id, visitor_hmac, session_hmac, person_id,
1903
+ started_at, last_active_at, landing_path, exit_path, pageview_count, event_count
1904
+ ) VALUES ($1,$2,$3,$4,$5,$6,$7,$7,$8,$8,$9,1)
1905
+ ON CONFLICT(site_id, session_hmac) DO UPDATE SET
1906
+ person_id = COALESCE(analytics_sessions.person_id, EXCLUDED.person_id),
1907
+ last_active_at = GREATEST(analytics_sessions.last_active_at, EXCLUDED.last_active_at),
1908
+ ended_at = GREATEST(COALESCE(analytics_sessions.ended_at, EXCLUDED.last_active_at), EXCLUDED.last_active_at),
1909
+ duration_ms = GREATEST(
1910
+ analytics_sessions.duration_ms,
1911
+ LEAST(1800000, (EXTRACT(EPOCH FROM (GREATEST(analytics_sessions.last_active_at, EXCLUDED.last_active_at) - analytics_sessions.started_at)) * 1000)::bigint)
1912
+ ),
1913
+ exit_path = EXCLUDED.exit_path,
1914
+ pageview_count = analytics_sessions.pageview_count + EXCLUDED.pageview_count,
1915
+ event_count = analytics_sessions.event_count + 1,
1916
+ updated_at = now()
1917
+ RETURNING id`,[y(),e.siteId,e.pixelId,t,i,e.personId??null,e.event.occurredAt,s,e.event.eventName==="page_view"?1:0]),r=z(e.event.clickIds);if(!!!(e.event.source||e.event.medium||e.event.campaign||e.event.referrer||Object.keys(r).length))return;let d=pe("sha256").update(JSON.stringify({siteId:e.siteId,sessionHmac:i,source:e.event.source??null,medium:e.event.medium??null,campaign:e.event.campaign??null,referrer:V(e.event.referrer),clickIds:r,path:s})).digest("hex"),_=await e.client.query(`INSERT INTO analytics_attribution_touches(
1918
+ id, site_id, session_id, person_id, visitor_hmac, occurred_at,
1919
+ source, medium, campaign, referrer, click_ids, landing_path, touch_hash
1920
+ ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13)
1921
+ ON CONFLICT(site_id, touch_hash) DO UPDATE SET
1922
+ person_id = COALESCE(analytics_attribution_touches.person_id, EXCLUDED.person_id)
1923
+ RETURNING id`,[y(),e.siteId,a.rows[0].id,e.personId??null,t,e.event.occurredAt,e.event.source?.slice(0,180)??null,e.event.medium?.slice(0,180)??null,e.event.campaign?.slice(0,240)??null,V(e.event.referrer),JSON.stringify(r),s,d]);await e.client.query(`UPDATE analytics_sessions
1924
+ SET first_touch_id = COALESCE(first_touch_id, $2), last_touch_id = $2, updated_at = now()
1925
+ WHERE id = $1`,[a.rows[0].id,_.rows[0].id])}async function mn(e){let t=l(),i=`air_${y()}`,n=G(e.origin),a=(await t.query(`SELECT p.id, p.site_id, p.status,
1926
+ COALESCE(
1927
+ (e.eligible AND e.access_expires_at > now()) OR e.trial_ends_at > now(),
1928
+ false
1929
+ ) AS entitled
1930
+ FROM analytics_pixels p
1931
+ JOIN analytics_sites s ON s.id = p.site_id
1932
+ LEFT JOIN analytics_thorbit_entitlements e ON e.user_id = s.owner_user_id
1933
+ WHERE p.public_id = $1`,[e.publicPixelId])).rows[0];if(!a||a.status!=="active"){let c=[a?"pixel_disabled":"pixel_unknown"];return await t.query(`INSERT INTO analytics_ingestion_receipts(id, request_id, site_id, pixel_id, hostname, rejected_count, reason_codes)
1934
+ VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb)`,[y(),i,a?.site_id??null,a?.id??null,n,e.events.length,JSON.stringify(c)]),{requestId:i,accepted:0,rejected:e.events.length,reasonCodes:c,hostname:n,siteId:a?.site_id??null,pixelId:a?.id??null}}if(!a.entitled){let c=["thorbit_entitlement_required"];return await t.query(`INSERT INTO analytics_ingestion_receipts(id, request_id, site_id, pixel_id, hostname, rejected_count, reason_codes)
1935
+ VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb)`,[y(),i,a.site_id,a.id,n,e.events.length,JSON.stringify(c)]),{requestId:i,accepted:0,rejected:e.events.length,reasonCodes:c,hostname:n,siteId:a.site_id,pixelId:a.id}}if(!n){let c=["origin_missing_or_invalid"];return await t.query(`INSERT INTO analytics_ingestion_receipts(id, request_id, site_id, pixel_id, rejected_count, reason_codes)
1936
+ VALUES ($1, $2, $3, $4, $5, $6::jsonb)`,[y(),i,a.site_id,a.id,e.events.length,JSON.stringify(c)]),{requestId:i,accepted:0,rejected:e.events.length,reasonCodes:c,hostname:n,siteId:a.site_id,pixelId:a.id}}if((await t.query(`INSERT INTO analytics_pixel_domains(id, pixel_id, hostname, state, rejected_count)
1937
+ VALUES ($1, $2, $3, 'observed_unverified', 0)
1938
+ ON CONFLICT(pixel_id, hostname) DO UPDATE
1939
+ SET last_seen_at = now(), updated_at = now()
1940
+ RETURNING state`,[y(),a.id,n])).rows[0]?.state!=="approved"){await t.query(`UPDATE analytics_pixel_domains
1941
+ SET rejected_count = rejected_count + $3, last_seen_at = now(), updated_at = now()
1942
+ WHERE pixel_id = $1 AND hostname = $2`,[a.id,n,e.events.length]);let c=["domain_unapproved"];return await t.query(`INSERT INTO analytics_ingestion_receipts(id, request_id, site_id, pixel_id, hostname, rejected_count, reason_codes)
1943
+ VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb)`,[y(),i,a.site_id,a.id,n,e.events.length,JSON.stringify(c)]),{requestId:i,accepted:0,rejected:e.events.length,reasonCodes:c,hostname:n,siteId:a.site_id,pixelId:a.id}}let o=await t.connect(),d=0,_=0,u=new Set;try{await o.query("BEGIN");for(let c of e.events)try{await ei(o,{siteId:a.site_id,pixelId:a.id,event:c});let E=Re(c),N=c.identity,A=null,v=null;if(c.eventDefinitionId&&c.eventDefinitionVersion){let T=await o.query(`SELECT id, version FROM analytics_event_definitions
1944
+ WHERE id=$1 AND site_id=$2 AND version=$3 AND event_name=$4 AND status='active'
1945
+ LIMIT 1`,[c.eventDefinitionId,a.site_id,c.eventDefinitionVersion,c.eventName]);T.rows[0]&&(A=T.rows[0].id,v=T.rows[0].version)}let R=Ze(c)&&(c.visitorId||c.sessionId)?await o.query(`SELECT e.person_id FROM analytics_identity_nodes n JOIN analytics_identity_edges e ON e.identity_node_id = n.id
1946
+ WHERE n.site_id = $1 AND ((n.kind = 'visitor_id' AND n.value_hmac = $2) OR (n.kind = 'session_id' AND n.value_hmac = $3))
1947
+ ORDER BY e.confidence DESC, e.last_seen_at DESC LIMIT 1`,[a.site_id,c.visitorId?L("visitor_id",c.visitorId):"",c.sessionId?L("session_id",c.sessionId):""]):{rows:[]};if((await o.query(`INSERT INTO analytics_events(
1948
+ id, event_id, site_id, pixel_id, hostname, visitor_id, session_id, event_name,
1949
+ occurred_at, path, canonical_url, title, referrer, source, medium, campaign,
1950
+ device_class, properties, country_code, region_code, click_ids, person_id,
1951
+ channel_family, platform, campaign_id, ad_set_id, ad_id, creative_id, placement, utm_term, utm_content,
1952
+ engaged_ms, scroll_depth, visitor_hmac, session_hmac, event_definition_id, event_definition_version
1953
+ ) VALUES (
1954
+ $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18::jsonb, $19, $20, $21::jsonb, $22,
1955
+ $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37
1956
+ ) ON CONFLICT(site_id, event_id) DO NOTHING`,[y(),c.eventId,a.site_id,a.id,n,c.visitorId??null,c.sessionId??null,c.eventName,c.occurredAt,tt(c.path||c.canonicalUrl),V(c.canonicalUrl),c.title?.slice(0,500)??null,V(c.referrer),c.source?.slice(0,180)??null,c.medium?.slice(0,180)??null,c.campaign?.slice(0,240)??null,c.deviceClass??null,JSON.stringify(oi(c.properties)),Ye(e.geography?.countryCode,2),Ye(e.geography?.regionCode,8),JSON.stringify(z(c.clickIds)),R.rows[0]?.person_id??null,E.channelFamily,E.platform,E.campaignId,E.adSetId,E.adId,E.creativeId,E.placement,E.term,E.content,si(c.engagedMs),ai(c.scrollDepth),c.visitorId?L("visitor_id",c.visitorId):null,c.sessionId?L("session_id",c.sessionId):null,A,v])).rowCount){d+=1;let T=R.rows[0]?.person_id;await di({client:o,siteId:a.site_id,pixelId:a.id,personId:T,event:c}),T&&c.eventName==="identify"&&await at(o,{siteId:a.site_id,personId:T,visitorId:c.visitorId,sessionId:c.sessionId}),N&&Zt(c)&&await Ii(o,{siteId:a.site_id,visitorId:c.visitorId,sessionId:c.sessionId,email:N.email,phone:N.phone,customerId:N.customerId,orderId:N.orderId,clickIds:c.clickIds,occurredAt:c.occurredAt})}else _+=1,u.add("duplicate_event")}catch{_+=1,u.add("event_insert_failed")}await o.query(`UPDATE analytics_pixel_domains
1957
+ SET accepted_count = accepted_count + $3,
1958
+ rejected_count = rejected_count + $4,
1959
+ last_seen_at = now(), updated_at = now()
1960
+ WHERE pixel_id = $1 AND hostname = $2`,[a.id,n,d,_]),await o.query(`INSERT INTO analytics_ingestion_receipts(
1961
+ id, request_id, site_id, pixel_id, hostname, accepted_count, rejected_count, reason_codes
1962
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb)`,[y(),i,a.site_id,a.id,n,d,_,JSON.stringify([...u])]),await o.query("COMMIT")}catch(c){throw await o.query("ROLLBACK"),c}finally{o.release()}return{requestId:i,accepted:d,rejected:_,reasonCodes:[...u],hostname:n,siteId:a.site_id,pixelId:a.id}}function Ye(e,t){let i=e?.trim().toUpperCase();return i&&/^[A-Z0-9-]+$/.test(i)?i.slice(0,t):null}function F(e,t,i){return pe("sha256").update(JSON.stringify({scope:e,siteId:t,filters:i})).digest("base64url").slice(0,18)}function ci(e,t){if(!e)return 0;try{let i=JSON.parse(Buffer.from(e,"base64url").toString("utf8"));if(i.v!==1||i.f!==t||!Number.isInteger(i.o)||Number(i.o)<0)throw new Error("invalid");return Number(i.o)}catch{throw new m("invalid_cursor","The pagination cursor is invalid for these filters.")}}function $(e,t,i){if(!t)return{items:e,pageInfo:{hasNextPage:!1,nextCursor:null}};let n=Math.min(100,Math.max(1,Math.trunc(t?.limit??25))),s=ci(t?.cursor,i),a=e.slice(s,s+n),r=s+a.length<e.length;return{items:a,pageInfo:{hasNextPage:r,nextCursor:r?Buffer.from(JSON.stringify({v:1,f:i,o:s+a.length})).toString("base64url"):null}}}async function yn(e){let i=(await e.db.query(`SELECT revision FROM analytics_revenue_setup_revisions
1963
+ WHERE setup_id=$1 ORDER BY revision DESC LIMIT 1 FOR UPDATE`,[e.setupId])).rows[0]?.revision??0;if(i!==e.expectedRevision)throw new m("analytics_revenue_setup_stale_revision",`Revenue setup revision ${e.expectedRevision} is stale; current revision is ${i}.`,409);let n=i+1;return await e.db.query(`INSERT INTO analytics_revenue_setup_revisions(
1964
+ setup_id,site_id,revision,authority_id,state,plan,request_fingerprint,created_by_user_id
1965
+ ) VALUES($1,$2,$3,$4,$5,$6::jsonb,$7,$8)`,[e.setupId,e.siteId,n,e.authorityId??null,e.state,JSON.stringify(e.plan),e.requestFingerprint,e.createdByUserId]),n}async function pn(e){let t=await e.db.query(`SELECT version_id,version,request_fingerprint FROM analytics_authoritative_outcome_versions
1966
+ WHERE site_id=$1 AND transport_event_id=$2`,[e.siteId,e.transportEventId]);if(t.rows[0]){if(t.rows[0].request_fingerprint!==e.requestFingerprint)throw new m("analytics_authoritative_outcome_replay_conflict","The provider event was replayed with a different normalized body.",409);return{versionId:t.rows[0].version_id,version:t.rows[0].version,replay:!0}}let i=await e.db.query(`SELECT current_version FROM analytics_authoritative_outcomes
1967
+ WHERE outcome_id=$1 AND site_id=$2 FOR UPDATE`,[e.outcomeId,e.siteId]);if(!i.rows[0])throw new m("analytics_authoritative_outcome_not_found","Authoritative outcome not found.",404);let n=i.rows[0].current_version+1,s=y();return await e.db.query(`INSERT INTO analytics_authoritative_outcome_versions(
1968
+ version_id,outcome_id,site_id,version,transport_event_id,status,value_minor,currency,
1969
+ occurred_at,observed_at,provenance,receipt_ref,request_fingerprint
1970
+ ) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13)`,[s,e.outcomeId,e.siteId,n,e.transportEventId,e.status,e.valueMinor.toString(),e.currency.toUpperCase(),e.occurredAt,e.observedAt,JSON.stringify(e.provenance),e.receiptRef,e.requestFingerprint]),await e.db.query(`UPDATE analytics_authoritative_outcomes
1971
+ SET current_version=$2,last_observed_at=$3 WHERE outcome_id=$1`,[e.outcomeId,n,e.observedAt]),{versionId:s,version:n,replay:!1}}async function Tn(e){let i=(await e.db.query(`SELECT outcome.conversion_id,authority.authority_role,
1972
+ authority.state AS authority_state
1973
+ FROM analytics_authoritative_outcomes outcome
1974
+ JOIN analytics_revenue_authorities authority
1975
+ ON authority.authority_id=outcome.authority_id
1976
+ AND authority.site_id=outcome.site_id
1977
+ JOIN analytics_authoritative_outcome_versions version
1978
+ ON version.outcome_id=outcome.outcome_id
1979
+ AND version.site_id=outcome.site_id
1980
+ JOIN analytics_provider_customer_bindings binding
1981
+ ON binding.authority_id=authority.authority_id
1982
+ AND binding.site_id=authority.site_id
1983
+ AND binding.person_id=outcome.person_id
1984
+ AND binding.verification_status='verified'
1985
+ WHERE outcome.site_id=$1 AND outcome.outcome_id=$2
1986
+ AND outcome.authority_id=$3 AND outcome.authority_key=$4
1987
+ AND version.version_id=$5 AND outcome.person_id=$6
1988
+ FOR UPDATE OF outcome`,[e.siteId,e.outcomeId,e.authorityId,e.authorityKey,e.authorityVersionId,e.personId])).rows[0];if(!i)throw new m("analytics_revenue_projection_evidence_invalid","The authoritative outcome, version, and customer binding could not be verified.",409);if(i.authority_role!=="primary"||!["verified","live","degraded"].includes(i.authority_state))throw new m("analytics_revenue_projection_authority_ineligible","Only an active primary revenue authority may project conversions.",409);let s=(await e.db.query(`SELECT conversion.id
1989
+ FROM analytics_conversions conversion
1990
+ JOIN analytics_authoritative_outcomes outcome
1991
+ ON outcome.conversion_id=conversion.id
1992
+ AND outcome.site_id=conversion.site_id
1993
+ WHERE conversion.site_id=$1 AND conversion.authority_key=$2
1994
+ AND outcome.outcome_id=$3 AND outcome.authority_id=$4
1995
+ FOR UPDATE OF conversion`,[e.siteId,e.authorityKey,e.outcomeId,e.authorityId])).rows[0]?.id,a=!1,r=!1;if(s)await e.db.query(`UPDATE analytics_conversions
1996
+ SET value_minor=$2,currency=$3,occurred_at=$4,person_id=$5,
1997
+ authority_version_id=$6,value_source='restatement',
1998
+ value_provenance=$7::jsonb,restated_at=now()
1999
+ WHERE id=$1 AND site_id=$8 AND authority_id=$9 AND authority_key=$10`,[s,e.valueMinor.toString(),e.currency.toUpperCase(),e.occurredAt,e.personId,e.authorityVersionId,JSON.stringify(e.valueProvenance),e.siteId,e.authorityId,e.authorityKey]),r=!0;else{let d=y();if(s=(await e.db.query(`INSERT INTO analytics_conversions(
2000
+ id,source_event_id,site_id,conversion_kind,value_minor,currency,
2001
+ occurred_at,person_id,authority_id,authority_key,authority_version_id,
2002
+ value_source,value_provenance
2003
+ ) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,'verified_revenue',$12::jsonb)
2004
+ ON CONFLICT(site_id,authority_key) WHERE authority_key IS NOT NULL
2005
+ DO NOTHING RETURNING id`,[d,e.sourceEventId,e.siteId,e.conversionKind,e.valueMinor.toString(),e.currency.toUpperCase(),e.occurredAt,e.personId,e.authorityId,e.authorityKey,e.authorityVersionId,JSON.stringify(e.valueProvenance)])).rows[0]?.id,a=!!s,!s){let u=await e.db.query(`SELECT conversion.id
2006
+ FROM analytics_conversions conversion
2007
+ JOIN analytics_authoritative_outcomes outcome
2008
+ ON outcome.conversion_id=conversion.id
2009
+ WHERE conversion.site_id=$1 AND conversion.authority_key=$2
2010
+ AND outcome.outcome_id=$3 AND outcome.authority_id=$4
2011
+ FOR UPDATE OF conversion`,[e.siteId,e.authorityKey,e.outcomeId,e.authorityId]);if(!u.rows[0])throw new m("analytics_conversion_authority_conflict","The authority key is already projected from a different authoritative outcome.",409);s=u.rows[0].id}await e.db.query(`UPDATE analytics_authoritative_outcomes SET conversion_id=$2
2012
+ WHERE outcome_id=$1 AND site_id=$3
2013
+ AND (conversion_id IS NULL OR conversion_id=$2)`,[e.outcomeId,s,e.siteId])}let o=0;if(a&&e.activationPayloadCiphertext){let d=await e.db.query(`SELECT id,event_mapping FROM analytics_activation_destinations
2014
+ WHERE site_id=$1 AND status='active'
2015
+ AND automatic_delivery_enabled=true AND readiness IN ('verified','live')`,[e.siteId]);for(let _ of d.rows.filter(u=>Ae(u.event_mapping,e.conversionKind))){let u=await e.db.query(`INSERT INTO analytics_activation_jobs(
2016
+ id,destination_id,conversion_id,person_id,payload_ciphertext
2017
+ ) VALUES($1,$2,$3,$4,$5)
2018
+ ON CONFLICT(destination_id,conversion_id) DO NOTHING`,[y(),_.id,s,e.personId,e.activationPayloadCiphertext]);o+=u.rowCount??0}}return{id:s,created:a,restated:r,queuedDestinations:o}}async function Ln(e){let i=(await e.db.query(`SELECT invite_id,survey_id,survey_revision,conversion_id,exchange_expires_at,exchanged_at
2019
+ FROM analytics_post_purchase_survey_invites
2020
+ WHERE site_id=$1 AND invitation_token_digest=$2 FOR UPDATE`,[e.siteId,e.invitationTokenDigest])).rows[0];if(!i)throw new m("analytics_survey_invite_not_found","Survey invitation not found.",404);if(i.exchanged_at)throw new m("analytics_survey_invite_consumed","Survey invitation has already been exchanged.",409);if(Date.parse(new Date(i.exchange_expires_at).toISOString())<=Date.parse(e.now))throw new m("analytics_survey_exchange_expired","Survey exchange code has expired.",410);return await e.db.query(`UPDATE analytics_post_purchase_survey_invites
2021
+ SET exchanged_at=$2,session_token_digest=$3,session_expires_at=$4
2022
+ WHERE invite_id=$1`,[i.invite_id,e.now,e.sessionTokenDigest,e.sessionExpiresAt]),{inviteId:i.invite_id,surveyId:i.survey_id,surveyRevision:i.survey_revision,conversionId:i.conversion_id}}async function An(e){let i=await l().connect();try{if(await i.query("BEGIN"),await D(i,e.siteId,e.userId),e.pixelId&&!(await i.query("SELECT 1 FROM analytics_pixels WHERE id = $1 AND site_id = $2",[e.pixelId,e.siteId])).rowCount)throw new m("analytics_pixel_not_found","Pixel not found.",404);let n=e.personId?{rows:[{person_id:e.personId}]}:e.sessionId?await i.query(`SELECT e.person_id FROM analytics_identity_nodes n JOIN analytics_identity_edges e ON e.identity_node_id = n.id
2023
+ WHERE n.site_id = $1 AND n.kind = 'session_id' AND n.value_hmac = $2
2024
+ ORDER BY e.confidence DESC LIMIT 1`,[e.siteId,L("session_id",e.sessionId)]):{rows:[]},s=e.authority?await i.query(`SELECT id FROM analytics_conversions
2025
+ WHERE site_id=$1 AND authority_key=$2 FOR UPDATE`,[e.siteId,e.authority.authorityKey]):{rows:[],rowCount:0},a=!1,r=!1,o;if(s.rows[0])o=s.rows[0].id,await i.query(`UPDATE analytics_conversions
2026
+ SET value_minor=$2, currency=$3, occurred_at=$4,
2027
+ value_source='restatement', value_provenance=$5::jsonb,
2028
+ authority_id=$6, authority_version_id=$7, restated_at=now()
2029
+ WHERE id=$1`,[o,e.valueMinor??0,(e.currency||"USD").toUpperCase(),e.occurredAt,JSON.stringify(e.authority?.valueProvenance??{}),e.authority?.authorityId,e.authority?.authorityVersionId??null]),r=!0;else{let _=y(),u=await i.query(`INSERT INTO analytics_conversions(
2030
+ id, source_event_id, site_id, pixel_id, session_id, conversion_kind,
2031
+ value_minor, currency, occurred_at, person_id, order_id, session_hmac,
2032
+ authority_id, authority_key, authority_version_id, value_source, value_provenance
2033
+ ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17::jsonb)
2034
+ ON CONFLICT(site_id, source_event_id) DO NOTHING RETURNING id`,[_,e.sourceEventId,e.siteId,e.pixelId??null,e.sessionId??null,e.conversionKind,e.valueMinor??0,(e.currency||"USD").toUpperCase(),e.occurredAt,n.rows[0]?.person_id??null,e.orderId??null,e.sessionId?L("session_id",e.sessionId):null,e.authority?.authorityId??null,e.authority?.authorityKey??null,e.authority?.authorityVersionId??null,e.authority?"verified_revenue":"default_zero",JSON.stringify(e.authority?.valueProvenance??{})]);if(a=!!u.rowCount,a)o=u.rows[0].id;else{let c=await i.query("SELECT id,authority_key FROM analytics_conversions WHERE site_id=$1 AND source_event_id=$2 FOR UPDATE",[e.siteId,e.sourceEventId]);if(!c.rows[0])throw new m("analytics_conversion_conflict","Conversion conflict could not be resolved.",409);if(e.authority&&c.rows[0].authority_key!==e.authority.authorityKey)throw new m("analytics_conversion_authority_conflict","The source event is already bound to a different revenue authority.",409);o=c.rows[0].id}}let d=0;if(a&&e.activationPayloadCiphertext){let _=await i.query(`SELECT id,event_mapping FROM analytics_activation_destinations
2035
+ WHERE site_id = $1 AND status = 'active' AND automatic_delivery_enabled=true
2036
+ AND readiness IN ('verified','live')`,[e.siteId]);for(let u of _.rows.filter(c=>Ae(c.event_mapping,e.conversionKind))){let c=await i.query(`INSERT INTO analytics_activation_jobs(
2037
+ id, destination_id, conversion_id, person_id, payload_ciphertext
2038
+ ) VALUES ($1,$2,$3,$4,$5)
2039
+ ON CONFLICT(destination_id, conversion_id) DO NOTHING`,[y(),u.id,o,n.rows[0]?.person_id??null,e.activationPayloadCiphertext]);d+=c.rowCount??0}}return await i.query("COMMIT"),{id:o,created:a,restated:r,queuedDestinations:d}}catch(n){throw await i.query("ROLLBACK"),n}finally{i.release()}}function j(e,t){let i=[e,t.start,t.end],n=["e.site_id = $1","e.occurred_at >= $2::timestamptz","e.occurred_at < $3::timestamptz"],s=(a,r)=>{i.push(r),n.push(a.replace("?",`$${i.length}`))};return t.pixelIds?.length&&s("e.pixel_id = ANY(?::uuid[])",t.pixelIds),t.hostnames?.length&&s("e.hostname = ANY(?::text[])",t.hostnames),t.source&&s("COALESCE(e.source, '(direct)') = ?",t.source),t.medium&&s("COALESCE(e.medium, '(none)') = ?",t.medium),t.campaign&&s("COALESCE(e.campaign, '(none)') = ?",t.campaign),t.eventName&&s("e.event_name = ?",t.eventName),t.channelFamily&&s("COALESCE(e.channel_family, 'other') = ?",t.channelFamily),t.platform&&s("COALESCE(e.platform, e.source, '(direct)') = ?",t.platform),t.referrer&&s("COALESCE(e.referrer, '') ILIKE '%' || ? || '%'",t.referrer),t.path&&s("e.path = ?",t.path),t.deviceClass&&s("COALESCE(e.device_class, 'unknown') = ?",t.deviceClass),t.countryCode&&s("COALESCE(e.country_code, 'unknown') = ?",t.countryCode.toUpperCase()),t.regionCode&&s("COALESCE(e.region_code, 'unknown') = ?",t.regionCode.toUpperCase()),{where:n.join(" AND "),values:i}}function de(e,t){let i=[e,t.start,t.end],n=["c.site_id = $1","c.occurred_at >= $2::timestamptz","c.occurred_at < $3::timestamptz"],s=(a,r)=>{i.push(r),n.push(a.replaceAll("?",`$${i.length}`))};return t.pixelIds?.length&&s("c.pixel_id = ANY(?::uuid[])",t.pixelIds),t.hostnames?.length&&s("EXISTS (SELECT 1 FROM analytics_events ce WHERE ce.site_id = c.site_id AND ce.session_id = c.session_id AND ce.hostname = ANY(?::text[]))",t.hostnames),t.source&&s("COALESCE(first_event.source, '(direct)') = ?",t.source),t.medium&&s("COALESCE(first_event.medium, '(none)') = ?",t.medium),t.campaign&&s("COALESCE(first_event.campaign, '(none)') = ?",t.campaign),t.eventName&&s("EXISTS (SELECT 1 FROM analytics_events ce WHERE ce.site_id = c.site_id AND ce.session_id = c.session_id AND ce.event_name = ?)",t.eventName),t.channelFamily&&s("EXISTS (SELECT 1 FROM analytics_events ce WHERE ce.site_id = c.site_id AND ce.session_id = c.session_id AND COALESCE(ce.channel_family, 'other') = ?)",t.channelFamily),t.platform&&s("EXISTS (SELECT 1 FROM analytics_events ce WHERE ce.site_id = c.site_id AND ce.session_id = c.session_id AND COALESCE(ce.platform, ce.source, '(direct)') = ?)",t.platform),t.referrer&&s("EXISTS (SELECT 1 FROM analytics_events ce WHERE ce.site_id = c.site_id AND ce.session_id = c.session_id AND COALESCE(ce.referrer, '') ILIKE '%' || ? || '%')",t.referrer),t.path&&s("EXISTS (SELECT 1 FROM analytics_events ce WHERE ce.site_id = c.site_id AND ce.session_id = c.session_id AND ce.path = ?)",t.path),t.deviceClass&&s("EXISTS (SELECT 1 FROM analytics_events ce WHERE ce.site_id = c.site_id AND ce.session_id = c.session_id AND COALESCE(ce.device_class, 'unknown') = ?)",t.deviceClass),t.countryCode&&s("EXISTS (SELECT 1 FROM analytics_events ce WHERE ce.site_id = c.site_id AND ce.session_id = c.session_id AND COALESCE(ce.country_code, 'unknown') = ?)",t.countryCode.toUpperCase()),t.regionCode&&s("EXISTS (SELECT 1 FROM analytics_events ce WHERE ce.site_id = c.site_id AND ce.session_id = c.session_id AND COALESCE(ce.region_code, 'unknown') = ?)",t.regionCode.toUpperCase()),t.conversionKind&&s("c.conversion_kind = ?",t.conversionKind),{where:n.join(" AND "),values:i}}async function We(e,t,i){let n=l();await g(n,e,t);let s=j(e,i),a=await n.query(`SELECT count(DISTINCT visitor_id)::int AS visitors,
2040
+ count(DISTINCT session_id)::int AS sessions,
2041
+ count(*) FILTER (WHERE event_name = 'page_view')::int AS pageviews,
2042
+ count(*)::int AS events,
2043
+ min(occurred_at)::text AS first_event_at,
2044
+ max(occurred_at)::text AS last_event_at
2045
+ FROM analytics_events e WHERE ${s.where}`,s.values),r=de(e,i),o=await n.query(`SELECT count(*)::int AS conversions, COALESCE(sum(c.value_minor), 0)::int AS revenue_minor
2046
+ FROM analytics_conversions c
2047
+ LEFT JOIN LATERAL (
2048
+ SELECT e.source, e.medium, e.campaign FROM analytics_events e
2049
+ WHERE e.site_id = c.site_id AND e.session_id = c.session_id
2050
+ ORDER BY e.occurred_at ASC LIMIT 1
2051
+ ) first_event ON true
2052
+ WHERE ${r.where}`,r.values),d=a.rows[0],_=o.rows[0];return{...d,conversions:_.conversions,revenueMinor:_.revenue_minor,conversionRate:d.sessions>0?_.conversions/d.sessions:0,dataFreshThrough:d.last_event_at}}async function In(e,t,i){let n=l();await g(n,e,t);let s=j(e,i),a=i.timezone||"UTC",r=`$${s.values.length+1}`,o=await n.query(`SELECT date_trunc('day', e.occurred_at AT TIME ZONE ${r})::date::text AS date,
2053
+ count(DISTINCT e.visitor_id)::int AS visitors,
2054
+ count(DISTINCT e.session_id)::int AS sessions,
2055
+ count(*) FILTER (WHERE e.event_name = 'page_view')::int AS pageviews,
2056
+ count(*)::int AS events
2057
+ FROM analytics_events e
2058
+ WHERE ${s.where}
2059
+ GROUP BY 1 ORDER BY 1`,[...s.values,a]),d=de(e,i),_=`$${d.values.length+1}`,u=await n.query(`SELECT date_trunc('day', c.occurred_at AT TIME ZONE ${_})::date::text AS date,
2060
+ count(*)::int AS conversions,
2061
+ COALESCE(sum(c.value_minor), 0)::bigint AS revenue_minor
2062
+ FROM analytics_conversions c
2063
+ LEFT JOIN LATERAL (
2064
+ SELECT e.source, e.medium, e.campaign FROM analytics_events e
2065
+ WHERE e.site_id = c.site_id AND e.session_id = c.session_id
2066
+ ORDER BY e.occurred_at ASC LIMIT 1
2067
+ ) first_event ON true
2068
+ WHERE ${d.where}
2069
+ GROUP BY 1 ORDER BY 1`,[...d.values,a]),c=new Map;for(let N of o.rows)c.set(String(N.date),{...N,conversions:0,revenueMinor:0});for(let N of u.rows){let A=c.get(N.date)??{date:N.date,visitors:0,sessions:0,pageviews:0,events:0};c.set(N.date,{...A,conversions:N.conversions,revenueMinor:Number(N.revenue_minor)})}let E=[...c.values()].sort((N,A)=>String(N.date).localeCompare(String(A.date)));return{points:E,timezone:a,dataFreshThrough:E.at(-1)?.date??null}}async function _i(e,t,i,n){let s=l();await g(s,e,t);let a=j(e,i),r=i.journeyTier==="best_guess"?`${a.where} AND EXISTS (
2070
+ SELECT 1 FROM analytics_sessions candidate_session
2071
+ JOIN analytics_attribution_touches candidate_touch
2072
+ ON candidate_touch.session_id=candidate_session.id AND candidate_touch.site_id=candidate_session.site_id
2073
+ JOIN analytics_candidate_attribution_touches candidate_overlay
2074
+ ON candidate_overlay.touch_id=candidate_touch.id AND candidate_overlay.site_id=candidate_touch.site_id
2075
+ JOIN analytics_candidate_associations candidate_association
2076
+ ON candidate_association.id=candidate_overlay.association_id
2077
+ AND candidate_association.namespace_id=candidate_overlay.namespace_id
2078
+ WHERE candidate_session.site_id=e.site_id AND candidate_session.session_hmac=e.session_hmac
2079
+ AND candidate_overlay.eligibility='overlay_only' AND candidate_overlay.expires_at > now()
2080
+ AND candidate_association.status='active' AND candidate_association.confidence_band='high'
2081
+ AND candidate_association.expires_at > now()
2082
+ )`:`${a.where} AND e.person_id IS NOT NULL`,o=await s.query(`SELECT COALESCE(e.source, '(direct)') AS source,
2083
+ COALESCE(e.medium, '(none)') AS medium,
2084
+ COALESCE(e.campaign, '(none)') AS campaign,
2085
+ COALESCE(e.platform, e.source, '(direct)') AS platform,
2086
+ e.campaign_id, e.ad_set_id, e.ad_id, e.creative_id,
2087
+ count(DISTINCT e.visitor_id)::int AS visitors,
2088
+ count(DISTINCT e.session_id)::int AS sessions,
2089
+ count(*) FILTER (WHERE e.event_name = 'page_view')::int AS pageviews
2090
+ FROM analytics_events e
2091
+ WHERE ${r}
2092
+ GROUP BY 1, 2, 3, 4, 5, 6, 7, 8
2093
+ ORDER BY sessions DESC, visitors DESC
2094
+ LIMIT 1000`,a.values),[d,_]=await Promise.all([Be({client:s,siteId:e,from:i.start,to:i.end}),i.journeyTier==="best_guess"||i.journeyTier==="all"?ze({client:s,siteId:e,from:i.start,to:i.end}):Promise.resolve({touches:[],conversions:[],spendFacts:[]})]),u=I=>(!i.source||(I.source??"(direct)")===i.source)&&(!i.medium||(I.medium??"(none)")===i.medium)&&(!i.campaign||(I.campaignName??"(none)")===i.campaign)&&(!i.platform||(I.platform??I.source??"(direct)")===i.platform);d.touches=d.touches.filter(u),_.touches=_.touches.filter(u),i.conversionKind&&(d.conversions=d.conversions.filter(I=>I.outcome===i.conversionKind),_.conversions=_.conversions.filter(I=>I.outcome===i.conversionKind));let c={model:i.attributionModel??"position_based",clickWindow:i.clickWindow??90,viewWindow:i.viewWindow??30,customWeights:i.customWeights},E={touches:[...d.touches,..._.touches],conversions:[...d.conversions,..._.conversions],spendFacts:d.spendFacts},N=me({...E,configuration:c,granularity:"campaign"}),A=i.journeyTier==="best_guess"?"candidate":"confirmed",v=A==="candidate"?N.candidateAssist.credits:N.confirmed.credits,R=A==="candidate"?N.candidateAssist.rows:N.confirmed.rows,C=Ge(v,E.touches),T=new Map(o.rows.map(I=>[`${I.source}\0${I.medium}\0${I.campaign}`,I])),O=new Map(R.map(I=>[`${I.platform??""}\0${I.campaignId??""}`,I])),k=[...new Set([...T.keys(),...C.keys()])].map(I=>{let b=T.get(I),h=C.get(I),ve=h?.platform??b?.platform??null,Oe=h?.campaignId??b?.campaign_id??null,M=O.get(`${ve??""}\0${Oe??""}`),ce=Number(b?.sessions??0);return{source:h?.source??b?.source??"(direct)",medium:h?.medium??b?.medium??"(none)",campaign:h?.campaign??b?.campaign??"(none)",platform:ve,campaignId:Oe,adSetGroupId:b?.ad_set_id??null,adId:b?.ad_id??null,creativeId:b?.creative_id??null,visitors:Number(b?.visitors??0),sessions:ce,pageviews:Number(b?.pageviews??0),leads:h?.leads??0,qualifiedLeads:h?.qualifiedLeads??0,conversions:h?.conversions??0,customers:h?.customers??0,conversionRate:ce>0?(h?.conversions??0)/ce:0,revenueMinor:h?.revenueMinor??0,spendMinor:M?.spendMinor==null?null:Number(M.spendMinor),cplMinor:M?.cplMinor??null,cpqlMinor:M?.cpqlMinor??null,cpaMinor:M?.cpaMinor??null,cacMinor:M?.cacMinor??null,roas:M?.roas??null,scoreEligible:null,propensityEligible:null,coveragePercent:h&&h.attributedCredit>0?1:0,currency:M?.currency??v.find(dt=>dt.currency)?.currency??null}});k.sort((I,b)=>Number(b.sessions)-Number(I.sessions)||Number(b.conversions)-Number(I.conversions));let ge=$(k,n,F("acquisition",e,i));return{channels:ge.items,candidateAssistChannels:i.journeyTier==="all"?[...Ge(N.candidateAssist.credits,E.touches).values()]:void 0,comparisons:ui(i.compareModels?.length?Xe({...E,configurations:[c,...i.compareModels.map(I=>({...c,model:I,id:I}))],granularity:"campaign"}).projections:[N],A),attribution:{model:N.configuration.model,clickWindow:N.configuration.clickWindow,viewWindow:N.configuration.viewWindow,journeyTier:i.journeyTier??"confirmed",confirmedAndCandidateTotalsSeparated:!0},pageInfo:ge.pageInfo,dataFreshThrough:new Date().toISOString()}}function Ge(e,t){let i=new Map(t.map(s=>[s.touchId,s])),n=new Map;for(let s of e){let a=s.touchId?i.get(s.touchId):void 0,r=a?.source??"(unattributed)",o=a?.medium??"(none)",d=a?.campaignName??"(none)",_=`${r}\0${o}\0${d}`,u=n.get(_)??{source:r,medium:o,campaign:d,platform:a?.platform??null,campaignId:a?.campaignId??null,leads:0,qualifiedLeads:0,conversions:0,customers:0,revenueMinor:0,attributedCredit:0},c=s.creditMicros/1e6;s.outcome==="lead"&&(u.leads+=c),s.outcome==="qualified"&&(u.qualifiedLeads+=c),s.outcome==="conversion"&&(u.conversions+=c),s.outcome==="customer"&&(u.customers+=c),u.attributedCredit+=c,u.revenueMinor+=Number(s.creditedValueMinor),n.set(_,u)}return n}function ui(e,t){return e.map(i=>{let n=t==="candidate"?i.candidateAssist:i.confirmed,s=new Set(n.credits.map(r=>r.conversionId)),a=new Set(n.credits.filter(r=>!r.unattributed).map(r=>r.conversionId));return{model:i.configuration.model,conversions:n.credits.reduce((r,o)=>r+o.creditMicros/1e6,0),revenueMinor:n.credits.reduce((r,o)=>r+Number(o.creditedValueMinor),0),coveragePercent:s.size>0?a.size/s.size:null}})}var Ei=`COALESCE(e.channel_family, CASE
2095
+ WHEN lower(COALESCE(e.source,'') || ' ' || COALESCE(e.referrer,'')) ~ '(perplexity|chatgpt|openai|claude|anthropic|grok|xai|ai[_ -]?overview|google[_ -]?ai|gemini)' THEN 'llm'
2096
+ WHEN lower(COALESCE(e.source,'') || ' ' || COALESCE(e.referrer,'')) ~ '(facebook|instagram|linkedin|tiktok|youtube|youtu\\.be|twitter|x\\.com)' THEN 'social'
2097
+ WHEN lower(COALESCE(e.source,'') || ' ' || COALESCE(e.referrer,'')) ~ '(^|[^a-z])(g2|bbb)([^a-z]|$)|trustpilot|capterra|yelp|better business bureau' THEN 'review'
2098
+ WHEN COALESCE(e.source,'') = '' THEN 'direct'
2099
+ WHEN lower(COALESCE(e.medium,'')) ~ 'email|newsletter' THEN 'email'
2100
+ ELSE 'other' END)`;async function gn(e,t,i){let n=l();await g(n,e,t);let s=j(e,i),a=await n.query(`
2101
+ SELECT ${Ei} AS family,
2102
+ COALESCE(e.platform, NULLIF(e.source,''), '(direct)') AS platform,
2103
+ COALESCE(NULLIF(e.campaign,''), '(none)') AS campaign,
2104
+ COALESCE(NULLIF(e.campaign_id,''), '(unknown)') AS campaign_id,
2105
+ COALESCE(NULLIF(e.ad_set_id,''), '(unknown)') AS ad_set_id,
2106
+ COALESCE(NULLIF(e.ad_id,''), '(unknown)') AS ad_id,
2107
+ COALESCE(NULLIF(e.creative_id,''), '(unknown)') AS creative_id,
2108
+ count(DISTINCT e.visitor_id)::int AS visitors,
2109
+ count(DISTINCT e.session_id)::int AS sessions,
2110
+ count(*) FILTER (WHERE e.event_name = 'page_view')::int AS pageviews,
2111
+ count(*)::int AS events
2112
+ FROM analytics_events e
2113
+ WHERE ${s.where}
2114
+ GROUP BY 1,2,3,4,5,6,7
2115
+ ORDER BY sessions DESC, events DESC
2116
+ LIMIT 2000`,s.values);return{sections:Object.fromEntries(["llm","social","review"].map(d=>[d,a.rows.filter(_=>_.family===d)])),rows:a.rows,dataFreshThrough:new Date().toISOString()}}var li=["path","visitors","sessions","pageviews","entrances","exits","engagement_events","conversions","avg_seconds_on_page","avg_scroll_depth","bounce_rate","exit_rate","engagement_rate","pageviews_per_session"];function Ni(e){return{sort:li.includes(e?.sort)?e.sort:"pageviews",direction:e?.direction==="asc"?"asc":"desc",search:e?.search?.trim().slice(0,300)||null}}async function mi(e,t,i,n,s){let a=l();await g(a,e,t);let r=j(e,i),o=Ni(s),d=`$${r.values.length+1}`,_=o.sort==="path"?`metrics.path ${o.direction==="asc"?"ASC":"DESC"}`:`${o.sort} ${o.direction==="asc"?"ASC":"DESC"} NULLS LAST, metrics.path ASC`,u=await a.query(`WITH filtered AS (
2117
+ SELECT e.* FROM analytics_events e WHERE ${r.where}
2118
+ ), page_metrics AS (
2119
+ SELECT path, count(DISTINCT visitor_id)::int AS visitors,
2120
+ count(DISTINCT session_id)::int AS sessions,
2121
+ count(*) FILTER (WHERE event_name = 'page_view')::int AS pageviews,
2122
+ count(*) FILTER (WHERE event_name NOT IN ('page_view', 'page_engagement'))::int AS engagement_events,
2123
+ count(*)::int AS events
2124
+ FROM filtered GROUP BY path
2125
+ ), session_bounds AS (
2126
+ SELECT session_id,
2127
+ (array_agg(path ORDER BY occurred_at ASC, id ASC))[1] AS entrance_path,
2128
+ (array_agg(path ORDER BY occurred_at DESC, id DESC))[1] AS exit_path
2129
+ FROM filtered WHERE session_id IS NOT NULL GROUP BY session_id
2130
+ ), session_pageviews AS (
2131
+ SELECT session_id, count(*) FILTER (WHERE event_name = 'page_view')::int AS pageviews
2132
+ FROM filtered WHERE session_id IS NOT NULL GROUP BY session_id
2133
+ ), page_dwell AS (
2134
+ SELECT session_id, path,
2135
+ sum(engaged_ms)::bigint AS engaged_ms,
2136
+ max(scroll_depth)::int AS scroll_depth
2137
+ FROM filtered
2138
+ WHERE event_name = 'page_engagement' AND session_id IS NOT NULL
2139
+ GROUP BY session_id, path
2140
+ ), dwell_metrics AS (
2141
+ SELECT path,
2142
+ avg(engaged_ms) FILTER (WHERE engaged_ms IS NOT NULL) AS avg_engaged_ms,
2143
+ avg(scroll_depth) FILTER (WHERE scroll_depth IS NOT NULL) AS avg_scroll,
2144
+ count(*) FILTER (WHERE engaged_ms IS NOT NULL)::int AS measured_pageviews
2145
+ FROM page_dwell GROUP BY path
2146
+ ), entrances AS (
2147
+ SELECT entrance_path AS path, count(*)::int AS entrances FROM session_bounds GROUP BY entrance_path
2148
+ ), exits AS (
2149
+ SELECT exit_path AS path, count(*)::int AS exits FROM session_bounds GROUP BY exit_path
2150
+ ), bounces AS (
2151
+ SELECT b.entrance_path AS path, count(*)::int AS bounces
2152
+ FROM session_bounds b JOIN session_pageviews p USING(session_id)
2153
+ WHERE p.pageviews <= 1
2154
+ GROUP BY b.entrance_path
2155
+ ), converted_sessions AS (
2156
+ SELECT DISTINCT c.session_id
2157
+ FROM analytics_conversions c
2158
+ WHERE c.site_id = $1 AND c.occurred_at >= $2::timestamptz AND c.occurred_at < $3::timestamptz
2159
+ AND c.session_id IS NOT NULL
2160
+ ), path_sessions AS (
2161
+ SELECT DISTINCT path, session_id FROM filtered WHERE session_id IS NOT NULL
2162
+ ), engaged AS (
2163
+ SELECT ps.path,
2164
+ count(*) FILTER (
2165
+ WHERE sp.pageviews >= 2
2166
+ OR COALESCE(pd.engaged_ms, 0) >= ${ni}
2167
+ OR cs.session_id IS NOT NULL
2168
+ )::int AS engaged_sessions
2169
+ FROM path_sessions ps
2170
+ JOIN session_pageviews sp USING(session_id)
2171
+ LEFT JOIN page_dwell pd ON pd.session_id = ps.session_id AND pd.path = ps.path
2172
+ LEFT JOIN converted_sessions cs ON cs.session_id = ps.session_id
2173
+ GROUP BY ps.path
2174
+ ), assisted AS (
2175
+ SELECT f.path, count(DISTINCT c.id)::int AS conversions
2176
+ FROM analytics_conversions c JOIN filtered f ON f.session_id = c.session_id
2177
+ WHERE c.site_id = $1 AND c.occurred_at >= $2::timestamptz AND c.occurred_at < $3::timestamptz
2178
+ GROUP BY f.path
2179
+ )
2180
+ SELECT metrics.*,
2181
+ COALESCE(entrances.entrances, 0)::int AS entrances,
2182
+ COALESCE(exits.exits, 0)::int AS exits,
2183
+ COALESCE(assisted.conversions, 0)::int AS conversions,
2184
+ COALESCE(dwell.measured_pageviews, 0)::int AS measured_pageviews,
2185
+ CASE WHEN dwell.avg_engaged_ms IS NULL THEN NULL
2186
+ ELSE round((dwell.avg_engaged_ms / 1000.0)::numeric, 1) END AS avg_seconds_on_page,
2187
+ CASE WHEN dwell.avg_scroll IS NULL THEN NULL
2188
+ ELSE round(dwell.avg_scroll::numeric, 1) END AS avg_scroll_depth,
2189
+ CASE WHEN COALESCE(entrances.entrances, 0) = 0 THEN NULL
2190
+ ELSE round(COALESCE(bounces.bounces, 0)::numeric * 100 / entrances.entrances, 1) END AS bounce_rate,
2191
+ CASE WHEN metrics.pageviews = 0 THEN NULL
2192
+ ELSE round(COALESCE(exits.exits, 0)::numeric * 100 / metrics.pageviews, 1) END AS exit_rate,
2193
+ CASE WHEN metrics.sessions = 0 THEN NULL
2194
+ ELSE round(COALESCE(engaged.engaged_sessions, 0)::numeric * 100 / metrics.sessions, 1) END AS engagement_rate,
2195
+ CASE WHEN metrics.sessions = 0 THEN NULL
2196
+ ELSE round(metrics.pageviews::numeric / metrics.sessions, 2) END AS pageviews_per_session
2197
+ FROM page_metrics metrics
2198
+ LEFT JOIN entrances USING(path)
2199
+ LEFT JOIN exits USING(path)
2200
+ LEFT JOIN assisted USING(path)
2201
+ LEFT JOIN bounces USING(path)
2202
+ LEFT JOIN engaged USING(path)
2203
+ LEFT JOIN dwell_metrics dwell USING(path)
2204
+ WHERE ${d}::text IS NULL OR metrics.path ILIKE '%' || ${d} || '%'
2205
+ ORDER BY ${_}
2206
+ LIMIT 1000`,[...r.values,o.search]),c=$(u.rows,n,F(`content:${o.sort}:${o.direction}:${o.search??""}`,e,i));return{content:c.items,pageInfo:c.pageInfo,table:o,dataFreshThrough:new Date().toISOString()}}async function vn(e,t,i,n){let s=l();await g(s,e,t);let a=j(e,i),[r,o]=await Promise.all([s.query(`SELECT e.event_name AS name, count(*)::int AS count, max(e.occurred_at)::text AS last_seen_at
2207
+ FROM analytics_events e
2208
+ WHERE ${a.where}
2209
+ GROUP BY e.event_name ORDER BY count DESC, e.event_name ASC
2210
+ LIMIT 1000`,a.values),s.query(`SELECT e.event_id, e.event_name, e.path, e.hostname,
2211
+ COALESCE(e.source, '(direct)') AS source,
2212
+ COALESCE(e.campaign, '(none)') AS campaign,
2213
+ e.occurred_at::text
2214
+ FROM analytics_events e WHERE ${a.where}
2215
+ ORDER BY e.occurred_at DESC, e.id DESC LIMIT 25`,a.values)]),d=[e,i.start,i.end],_=["r.site_id = $1","r.created_at >= $2::timestamptz","r.created_at < $3::timestamptz"],u=(N,A)=>{d.push(A),_.push(N.replace("?",`$${d.length}`))};i.pixelIds?.length&&u("r.pixel_id = ANY(?::uuid[])",i.pixelIds),i.hostnames?.length&&u("r.hostname = ANY(?::text[])",i.hostnames);let c=await s.query(`SELECT reason.code, count(*)::int AS requests,
2216
+ COALESCE(sum(r.rejected_count), 0)::int AS rejected_events,
2217
+ max(r.created_at)::text AS last_seen_at
2218
+ FROM analytics_ingestion_receipts r
2219
+ CROSS JOIN LATERAL jsonb_array_elements_text(r.reason_codes) AS reason(code)
2220
+ WHERE ${_.join(" AND ")} AND r.rejected_count > 0
2221
+ GROUP BY reason.code ORDER BY rejected_events DESC, reason.code ASC`,d),E=$(r.rows,n,F("events",e,i));return{events:E.items,samples:o.rows,rejections:c.rows,pageInfo:E.pageInfo,dataFreshThrough:o.rows[0]?.occurred_at??null}}async function yi(e,t,i,n){let s=l();await g(s,e,t);let a=de(e,i),r=await s.query(`SELECT c.id, c.source_event_id, c.pixel_id, c.session_id, c.conversion_kind,
2222
+ c.value_minor::int, c.currency, c.occurred_at::text,
2223
+ COALESCE(first_event.source, '(direct)') AS source,
2224
+ COALESCE(first_event.medium, '(none)') AS medium,
2225
+ COALESCE(first_event.campaign, '(none)') AS campaign
2226
+ FROM analytics_conversions c
2227
+ LEFT JOIN LATERAL (
2228
+ SELECT e.source, e.medium, e.campaign
2229
+ FROM analytics_events e
2230
+ WHERE e.site_id = c.site_id AND e.session_id = c.session_id
2231
+ ORDER BY e.occurred_at ASC LIMIT 1
2232
+ ) first_event ON true
2233
+ WHERE ${a.where}
2234
+ ORDER BY c.occurred_at DESC
2235
+ LIMIT 1000`,a.values),o=r.rows.reduce((_,u)=>({count:_.count+1,valueMinor:_.valueMinor+Number(u.value_minor??0)}),{count:0,valueMinor:0}),d=$(r.rows,n,F("conversions",e,i));return{conversions:d.items,pageInfo:d.pageInfo,summary:o,dataFreshThrough:r.rows[0]?.occurred_at??null}}async function On(e,t,i,n){let s=l();await g(s,e,t);let a=de(e,i),r=await s.query(`SELECT c.session_id, c.person_id,
2236
+ string_agg(COALESCE(path_event.platform, path_event.source, '(direct)') || ' / ' || COALESCE(path_event.medium, '(none)') ||
2237
+ CASE WHEN path_event.campaign IS NOT NULL THEN ' / ' || path_event.campaign ELSE '' END ||
2238
+ CASE WHEN path_event.ad_id IS NOT NULL THEN ' / ad:' || path_event.ad_id ELSE '' END,
2239
+ ' \u2192 ' ORDER BY path_event.occurred_at) AS path,
2240
+ count(path_event.id)::int AS touchpoints,
2241
+ 1::int AS conversions,
2242
+ c.value_minor::int AS value_minor
2243
+ FROM analytics_conversions c
2244
+ LEFT JOIN LATERAL (
2245
+ SELECT e.source, e.medium, e.campaign FROM analytics_events e
2246
+ WHERE e.site_id = c.site_id AND e.session_id = c.session_id
2247
+ ORDER BY e.occurred_at ASC LIMIT 1
2248
+ ) first_event ON true
2249
+ JOIN analytics_events path_event ON path_event.site_id = c.site_id AND
2250
+ ((c.person_id IS NOT NULL AND path_event.person_id = c.person_id) OR (c.person_id IS NULL AND path_event.session_id = c.session_id))
2251
+ WHERE ${a.where}
2252
+ GROUP BY c.id
2253
+ ORDER BY conversions DESC, value_minor DESC
2254
+ LIMIT 1000`,a.values),o=$(r.rows,n,F("paths",e,i));return{paths:o.items,pageInfo:o.pageInfo,dataFreshThrough:new Date().toISOString()}}async function Sn(e,t,i,n,s){let a=l();await g(a,e,t);let r=j(e,i),o=n==="device"?"COALESCE(NULLIF(e.device_class, ''), 'unknown') AS key, COALESCE(NULLIF(e.device_class, ''), 'Unknown') AS label":n==="source"?"COALESCE(NULLIF(e.source, ''), '(direct)') AS key, COALESCE(NULLIF(e.source, ''), '(direct)') AS label":n==="country"?"COALESCE(NULLIF(e.country_code, ''), 'unknown') AS key, COALESCE(NULLIF(e.country_code, ''), 'Unknown') AS label":n==="region"?"COALESCE(NULLIF(e.region_code, ''), 'unknown') AS key, COALESCE(NULLIF(e.region_code, ''), 'Unknown') AS label":`extract(dow from e.occurred_at)::int::text || '-' || extract(hour from e.occurred_at)::int::text AS key,
2255
+ to_char(e.occurred_at, 'Dy') || ' ' || lpad(extract(hour from e.occurred_at)::int::text, 2, '0') || ':00' AS label`,d=await a.query(`SELECT ${o},
2256
+ count(DISTINCT e.visitor_id)::int AS visitors,
2257
+ count(DISTINCT e.session_id)::int AS sessions,
2258
+ count(*) FILTER (WHERE e.event_name = 'page_view')::int AS pageviews,
2259
+ count(*)::int AS events,
2260
+ count(*)::int AS value
2261
+ FROM analytics_events e
2262
+ WHERE ${r.where}
2263
+ GROUP BY 1, 2
2264
+ ORDER BY value DESC, key ASC
2265
+ LIMIT 1000`,r.values),_=$(d.rows,s,F(`dimension:${n}`,e,i));return{dimension:n,unit:"events",chartRecommendation:n==="device"?"donut":n==="weekday_hour"?"heatmap":n==="country"||n==="region"?"choropleth_or_bar":"horizontal_bar",rows:_.items,pageInfo:_.pageInfo,dataFreshThrough:new Date().toISOString()}}function it(e){let t=new URL(e.destination_url);for(let[i,n]of[["utm_source",e.utm_source],["utm_medium",e.utm_medium],["utm_campaign",e.utm_campaign],["utm_term",e.utm_term],["utm_content",e.utm_content],["ad_group",e.ad_group],["ad_name",e.ad_name],["creative_id",e.creative_id]])n&&t.searchParams.set(i,n);return t.toString()}function nt(e){return{...e,tagged_url:it(e)}}async function Cn(e){let t=l();await D(t,e.siteId,e.userId);let i;try{i=new URL(e.destinationUrl)}catch{throw new m("invalid_destination_url","Enter a valid destination URL.")}if(!["http:","https:"].includes(i.protocol))throw new m("invalid_destination_url","Destination URL must use HTTP or HTTPS.");if(e.pixelId&&!(await t.query("SELECT 1 FROM analytics_pixels WHERE id = $1 AND site_id = $2",[e.pixelId,e.siteId])).rowCount)throw new m("analytics_pixel_not_found","Pixel not found.",404);let n=(e.shortCode?.trim().toLowerCase().replace(/[^a-z0-9_-]/g,"-")||P(5).toString("base64url").toLowerCase()).slice(0,48);try{let s=await t.query(`INSERT INTO analytics_campaign_links(
2266
+ id, site_id, pixel_id, name, destination_url, short_code, utm_source, utm_medium,
2267
+ utm_campaign, utm_term, utm_content, ad_group, ad_name, creative_id, created_by_user_id
2268
+ ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)
2269
+ RETURNING *, 0::int AS click_count`,[y(),e.siteId,e.pixelId??null,e.name.trim(),i.toString(),n,e.source.trim(),e.medium.trim(),e.campaign.trim(),e.term?.trim()||null,e.content?.trim()||null,e.adGroup?.trim()||null,e.adName?.trim()||null,e.creativeId?.trim()||null,e.userId]);return nt(s.rows[0])}catch(s){throw s.code==="23505"?new m("campaign_short_code_taken","That short code is already in use.",409):s}}async function Rn(e,t,i){let n=l();await g(n,e,t);let a=(await n.query(`SELECT l.*, count(c.id)::int AS click_count
2270
+ FROM analytics_campaign_links l LEFT JOIN analytics_campaign_clicks c ON c.link_id = l.id
2271
+ WHERE l.site_id = $1
2272
+ GROUP BY l.id ORDER BY l.created_at DESC, l.id DESC LIMIT 1000`,[e])).rows.map(nt),o=$(a,i,F("campaign-links",e,{start:"all",end:"all"}));return{links:o.items,pageInfo:o.pageInfo}}async function fn(e){let t=l();if(await D(t,e.siteId,e.userId),!(await t.query("UPDATE analytics_campaign_links SET status = 'archived', updated_at = now() WHERE id = $1 AND site_id = $2",[e.linkId,e.siteId])).rowCount)throw new m("campaign_link_not_found","Campaign link not found.",404)}async function Dn(e,t){let i=l(),s=(await i.query("SELECT l.*, 0::int AS click_count FROM analytics_campaign_links l WHERE l.short_code = $1 AND l.status = 'active'",[e])).rows[0];return s?(await i.query("INSERT INTO analytics_campaign_clicks(id, link_id, referrer) VALUES ($1, $2, $3)",[y(),s.id,V(t||void 0)]),it(s)):null}async function Un(e){let t=l();if(await D(t,e.siteId,e.userId),!(await t.query("SELECT 1 FROM analytics_pixels WHERE id = $1 AND site_id = $2",[e.pixelId,e.siteId])).rowCount)throw new m("analytics_pixel_not_found","Pixel not found.",404);let s=`${Qe(e.name)}-${P(3).toString("hex")}`,a=`form_${P(18).toString("base64url")}`;return(await t.query(`INSERT INTO analytics_forms(id, public_id, site_id, pixel_id, name, slug, fields, brand, submit_label, success_message, redirect_url, consent_text, status, created_by_user_id)
2273
+ VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb,$8::jsonb,$9,$10,$11,$12,$13,$14)
2274
+ RETURNING *, 0::int AS submission_count`,[y(),a,e.siteId,e.pixelId,e.name.trim(),s,JSON.stringify(e.fields),JSON.stringify(e.brand),e.submitLabel?.trim()||"Submit",e.successMessage?.trim()||"Thank you. Your response was received.",e.redirectUrl?.trim()||null,e.consentText?.trim()||null,e.publish?"published":"draft",e.userId])).rows[0]}async function wn(e,t,i){let n=l();await g(n,e,t);let s=await n.query(`SELECT f.*, count(s.id)::int AS submission_count FROM analytics_forms f
2275
+ LEFT JOIN analytics_form_submissions s ON s.form_id = f.id
2276
+ WHERE f.site_id = $1 GROUP BY f.id ORDER BY f.created_at DESC, f.id DESC LIMIT 1000`,[e]),a=$(s.rows,i,F("forms",e,{start:"all",end:"all"}));return{forms:a.items,pageInfo:a.pageInfo}}async function bn(e){return(await l().query(`SELECT f.*, s.owner_user_id, p.public_id AS pixel_public_id,
2277
+ (SELECT count(*)::int FROM analytics_form_submissions fs WHERE fs.form_id = f.id) AS submission_count
2278
+ FROM analytics_forms f JOIN analytics_sites s ON s.id = f.site_id JOIN analytics_pixels p ON p.id = f.pixel_id
2279
+ WHERE f.public_id = $1 AND f.status = 'published'`,[e])).rows[0]??null}async function hn(e){let t=y();return await l().query(`INSERT INTO analytics_form_submissions(id, form_id, site_id, pixel_id, visitor_id, session_id, crm_person_ref, source, medium, campaign, crm_delivery_status, click_ids)
2280
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12::jsonb)`,[t,e.form.id,e.form.site_id,e.form.pixel_id,e.visitorId??null,e.sessionId??null,e.crmPersonRef??null,e.source?.slice(0,180)??null,e.medium?.slice(0,180)??null,e.campaign?.slice(0,240)??null,e.crmDeliveryStatus??"pending",JSON.stringify(z(e.clickIds))]),{submissionId:t}}var pi=new Set(["fbclid","gclid","gbraid","wbraid","ttclid","rdt_cid","msclkid","fbp","fbc","li_fat_id","snapclid"]);function z(e){let t={};for(let[i,n]of Object.entries(e??{})){if(!pi.has(i)||typeof n!="string")continue;let s=n.trim();s&&(t[i]=s.slice(0,500))}return t}function L(e,t){return Yt("sha256",Q()).update(`${e}:${t.trim().toLowerCase()}`).digest("hex")}async function Ti(e){for(;;){let i=await e.query(`SELECT id, visitor_id, session_id
2281
+ FROM analytics_events
2282
+ WHERE (visitor_id IS NOT NULL AND visitor_hmac IS NULL)
2283
+ OR (session_id IS NOT NULL AND session_hmac IS NULL)
2284
+ ORDER BY id
2285
+ LIMIT $1`,[500]);if(!i.rowCount)break;await e.query(`UPDATE analytics_events e
2286
+ SET visitor_hmac = COALESCE(e.visitor_hmac, patch.visitor_hmac),
2287
+ session_hmac = COALESCE(e.session_hmac, patch.session_hmac)
2288
+ FROM unnest($1::uuid[], $2::text[], $3::text[]) AS patch(id, visitor_hmac, session_hmac)
2289
+ WHERE e.id = patch.id`,[i.rows.map(n=>n.id),i.rows.map(n=>n.visitor_id?L("visitor_id",n.visitor_id):null),i.rows.map(n=>n.session_id?L("session_id",n.session_id):null)])}for(;;){let i=await e.query(`SELECT id, session_id
2290
+ FROM analytics_conversions
2291
+ WHERE session_id IS NOT NULL AND session_hmac IS NULL
2292
+ ORDER BY id
2293
+ LIMIT $1`,[500]);if(!i.rowCount)break;await e.query(`UPDATE analytics_conversions c
2294
+ SET session_hmac = patch.session_hmac
2295
+ FROM unnest($1::uuid[], $2::text[]) AS patch(id, session_hmac)
2296
+ WHERE c.id = patch.id AND c.session_hmac IS NULL`,[i.rows.map(n=>n.id),i.rows.map(n=>L("session_id",n.session_id))])}}function ye(){return`xrp_${P(18).toString("base64url")}`}function xn(e,t){let i=L("crm_entity_id",`${e}:${t}`).slice(0,32).split("");return i[12]="5",i[16]=["8","9","a","b"][Number.parseInt(i[16],16)%4],`${i.slice(0,8).join("")}-${i.slice(8,12).join("")}-${i.slice(12,16).join("")}-${i.slice(16,20).join("")}-${i.slice(20).join("")}`}async function st(e,t,i){let n=await e.query("SELECT id FROM analytics_identity_namespaces WHERE site_id=$1 AND status='approved' ORDER BY version DESC LIMIT 1",[t]);n.rows[0]||(await e.query(`INSERT INTO analytics_identity_namespaces(
2297
+ id,site_id,name,status,approved_hosts,approved_sources,version,approved_by_user_id,approved_at
2298
+ )
2299
+ SELECT $1,s.id,'X-Ray primary','approved',
2300
+ COALESCE((SELECT jsonb_agg(d.hostname ORDER BY d.hostname)
2301
+ FROM analytics_pixels p
2302
+ JOIN analytics_pixel_domains d ON d.pixel_id=p.id AND d.state='approved'
2303
+ WHERE p.site_id=s.id),'[]'::jsonb),
2304
+ $3::jsonb,1,s.owner_user_id,now()
2305
+ FROM analytics_sites s WHERE s.id=$2
2306
+ ON CONFLICT(site_id,name,version) DO NOTHING`,[y(),t,JSON.stringify([i])]),n=await e.query("SELECT id FROM analytics_identity_namespaces WHERE site_id=$1 AND status='approved' ORDER BY version DESC LIMIT 1",[t]));let s=n.rows[0]?.id;if(!s)throw new m("identity_namespace_unavailable","The Site identity graph could not be initialized.",409);return await e.query(`UPDATE analytics_identity_namespaces
2307
+ SET approved_sources=(SELECT jsonb_agg(value ORDER BY value)
2308
+ FROM (SELECT DISTINCT value
2309
+ FROM jsonb_array_elements_text(approved_sources || $3::jsonb)) merged_sources),
2310
+ approved_hosts=(SELECT COALESCE(jsonb_agg(hostname ORDER BY hostname),'[]'::jsonb)
2311
+ FROM (SELECT DISTINCT hostname
2312
+ FROM (SELECT jsonb_array_elements_text(approved_hosts) AS hostname
2313
+ UNION ALL
2314
+ SELECT d.hostname
2315
+ FROM analytics_pixels p
2316
+ JOIN analytics_pixel_domains d ON d.pixel_id=p.id AND d.state='approved'
2317
+ WHERE p.site_id=$2) hosts) distinct_hosts),
2318
+ updated_at=now()
2319
+ WHERE id=$1 AND site_id=$2`,[s,t,JSON.stringify([i])]),s}async function at(e,t){if(!t.visitorId&&!t.sessionId)return{events:0,sessions:0,touches:0,conversions:0};let i=t.visitorId?L("visitor_id",t.visitorId):null,n=t.sessionId?L("session_id",t.sessionId):null,s=await e.query(`UPDATE analytics_events SET person_id = $1 WHERE site_id = $2 AND person_id IS NULL
2320
+ AND (($3::text IS NOT NULL AND visitor_id = $3) OR ($4::text IS NOT NULL AND session_id = $4))`,[t.personId,t.siteId,t.visitorId??null,t.sessionId??null]),a=await e.query(`UPDATE analytics_conversions SET person_id = $1 WHERE site_id = $2 AND person_id IS NULL
2321
+ AND $3::text IS NOT NULL AND session_id = $3`,[t.personId,t.siteId,t.sessionId??null]),r=await e.query(`UPDATE analytics_sessions SET person_id = $1
2322
+ WHERE site_id = $2 AND person_id IS NULL
2323
+ AND (($3::text IS NOT NULL AND visitor_hmac = $3)
2324
+ OR ($4::text IS NOT NULL AND session_hmac = $4))`,[t.personId,t.siteId,i,n]),o=await e.query(`UPDATE analytics_attribution_touches SET person_id = $1
2325
+ WHERE site_id = $2 AND person_id IS NULL
2326
+ AND (($3::text IS NOT NULL AND visitor_hmac = $3)
2327
+ OR ($4::text IS NOT NULL AND session_id IN (
2328
+ SELECT id FROM analytics_sessions
2329
+ WHERE site_id = $2 AND session_hmac = $4
2330
+ )))`,[t.personId,t.siteId,i,n]);return{events:s.rowCount??0,sessions:r.rowCount??0,touches:o.rowCount??0,conversions:a.rowCount??0}}async function Te(e,t){let n=(await e.query(`SELECT id FROM analytics_people
2331
+ WHERE site_id=$1 AND (namespace_id=$2 OR namespace_id IS NULL)
2332
+ AND (($3::uuid IS NOT NULL AND crm_entity_id=$3::uuid)
2333
+ OR ($3::uuid IS NULL AND $4::text IS NOT NULL AND crm_person_ref=$4))
2334
+ ORDER BY first_seen_at,id LIMIT 1 FOR UPDATE`,[t.siteId,t.namespaceId,t.crmEntityId??null,t.crmPersonRef??null])).rows[0]?.id;if(!n&&!t.crmEntityId&&!t.crmPersonRef&&t.signals.length&&(n=(await e.query(`SELECT assignment.person_id
2335
+ FROM analytics_identity_nodes node
2336
+ JOIN analytics_identity_assignments assignment
2337
+ ON assignment.identity_node_id=node.id AND assignment.site_id=node.site_id
2338
+ WHERE node.site_id=$1 AND EXISTS (
2339
+ SELECT 1 FROM jsonb_to_recordset($2::jsonb) signal(kind text,value_hmac text)
2340
+ WHERE signal.kind=node.kind AND signal.value_hmac=node.value_hmac
2341
+ )
2342
+ ORDER BY assignment.assigned_at,assignment.person_id LIMIT 1`,[t.siteId,JSON.stringify(t.signals.map(a=>({kind:a.kind,value_hmac:a.valueHmac})))])).rows[0]?.person_id),n)await e.query(`UPDATE analytics_people SET last_seen_at=now(),
2343
+ namespace_id=COALESCE(namespace_id,$2),
2344
+ crm_entity_id=COALESCE(crm_entity_id,$3::uuid),
2345
+ public_ref=COALESCE(public_ref,$4)
2346
+ WHERE id=$1 AND site_id=$5`,[n,t.namespaceId,t.crmEntityId??null,ye(),t.siteId]);else{let s=t.crmEntityId??null,a=t.crmPersonRef??(s?`XRay::entity::${s}`:`XRay::anonymous::${y()}`);n=(await e.query(`INSERT INTO analytics_people(id,site_id,namespace_id,crm_person_ref,crm_entity_id,public_ref)
2347
+ VALUES($1,$2,$3,$4,$5::uuid,$6)
2348
+ ON CONFLICT(site_id,crm_person_ref) DO UPDATE SET last_seen_at=now(),
2349
+ namespace_id=COALESCE(analytics_people.namespace_id,EXCLUDED.namespace_id),
2350
+ crm_entity_id=COALESCE(analytics_people.crm_entity_id,EXCLUDED.crm_entity_id),
2351
+ public_ref=COALESCE(analytics_people.public_ref,EXCLUDED.public_ref)
2352
+ RETURNING id`,[y(),t.siteId,t.namespaceId,a,s,ye()])).rows[0].id}return{personId:n,mergedPersonIds:[]}}async function J(e,t){let n=(await e.query(`SELECT person_id,crm_entity_id::text FROM analytics_identity_assignments
2353
+ WHERE identity_node_id=$1 AND site_id=$2 FOR UPDATE`,[t.identityNodeId,t.siteId])).rows[0];if(!n){await e.query(`INSERT INTO analytics_identity_assignments(
2354
+ identity_node_id,site_id,person_id,crm_entity_id,evidence_kind
2355
+ ) VALUES($1,$2,$3,$4::uuid,$5)
2356
+ ON CONFLICT(identity_node_id) DO NOTHING`,[t.identityNodeId,t.siteId,t.personId,t.crmEntityId??null,t.evidenceKind]);let s=await e.query("SELECT person_id FROM analytics_identity_assignments WHERE identity_node_id=$1 AND site_id=$2",[t.identityNodeId,t.siteId]);if(s.rows[0]?.person_id===t.personId)return"assigned";if(!s.rows[0])throw new m("identity_assignment_unavailable","Identity assignment could not be recorded.",409);n={person_id:s.rows[0].person_id,crm_entity_id:null}}return n.person_id===t.personId?(await e.query(`UPDATE analytics_identity_assignments SET updated_at=now(),
2357
+ crm_entity_id=COALESCE(crm_entity_id,$3::uuid),evidence_kind=$4
2358
+ WHERE identity_node_id=$1 AND site_id=$2`,[t.identityNodeId,t.siteId,t.crmEntityId??null,t.evidenceKind]),"existing"):(await e.query(`INSERT INTO analytics_identity_assignment_conflicts(
2359
+ id,site_id,identity_node_id,active_person_id,attempted_person_id,
2360
+ active_crm_entity_id,attempted_crm_entity_id,evidence_kind
2361
+ ) VALUES($1,$2,$3,$4,$5,$6::uuid,$7::uuid,$8)
2362
+ ON CONFLICT(identity_node_id,active_person_id,attempted_person_id)
2363
+ WHERE status='pending' DO UPDATE SET observed_at=now(),evidence_kind=EXCLUDED.evidence_kind`,[y(),t.siteId,t.identityNodeId,n.person_id,t.personId,n.crm_entity_id,t.crmEntityId??null,t.evidenceKind]),"conflict")}async function Li(e,t){let n=(await e.query(`WITH person_nodes AS (
2364
+ SELECT DISTINCT identity_node_id
2365
+ FROM analytics_identity_edges
2366
+ WHERE site_id=$1 AND namespace_id=$2 AND person_id=$3
2367
+ )
2368
+ UPDATE analytics_candidate_associations a
2369
+ SET status='promoted',promoted_person_id=$3,promoted_at=$4::timestamptz,updated_at=now()
2370
+ WHERE a.site_id=$1 AND a.namespace_id=$2
2371
+ AND (a.left_identity_node_id IN (SELECT identity_node_id FROM person_nodes)
2372
+ OR a.right_identity_node_id IN (SELECT identity_node_id FROM person_nodes))
2373
+ AND (a.status IN ('active','expired') OR (a.status='promoted' AND a.promoted_person_id=$3))
2374
+ RETURNING a.id`,[t.siteId,t.namespaceId,t.personId,t.occurredAt])).rows.map(a=>a.id);if(!n.length)return{associationIds:[],touches:0};for(let a of n)await e.query(`INSERT INTO analytics_candidate_promotions(
2375
+ id,site_id,namespace_id,association_id,person_id,evidence_kind,
2376
+ evidence_ref_hmac,promoted_by,promoted_at
2377
+ ) VALUES($1,$2,$3,$4,$5,$6,$7,'system',$8::timestamptz)
2378
+ ON CONFLICT(association_id,person_id) DO NOTHING`,[y(),t.siteId,t.namespaceId,a,t.personId,t.candidateEvidenceKind,t.evidenceRefHmac,t.occurredAt]);let s=await e.query(`UPDATE analytics_attribution_touches touch SET person_id=$3
2379
+ FROM analytics_candidate_attribution_touches candidate_touch
2380
+ WHERE candidate_touch.site_id=$1 AND candidate_touch.namespace_id=$2
2381
+ AND candidate_touch.association_id=ANY($4::uuid[])
2382
+ AND candidate_touch.touch_id=touch.id AND touch.site_id=$1 AND touch.person_id IS NULL`,[t.siteId,t.namespaceId,t.personId,n]);return{associationIds:n,touches:s.rowCount??0}}async function Ai(e,t){let i=L("promotion_evidence",`${t.evidenceKind}:${t.evidenceRef}`),n=L("promotion_person",t.personId),s=L("promotion_receipt",`${t.siteId}:${t.personId}:${t.evidenceKind}:${t.evidenceRef}`);await e.query(`INSERT INTO analytics_identity_promotion_receipts(
2383
+ id,site_id,namespace_id,canonical_person_id,canonical_person_ref_hmac,
2384
+ evidence_kind,evidence_ref_hmac,prior_person_ids,prior_identity_node_ids,
2385
+ candidate_association_ids,affected_history_counts,schema_version,idempotency_key,promoted_at
2386
+ ) VALUES($1,$2,$3,$4,$5,$6,$7,$8::uuid[],$9::uuid[],$10::uuid[],$11::jsonb,
2387
+ 'xray.identity-promotion.v1',$12,$13::timestamptz)
2388
+ ON CONFLICT(site_id,idempotency_key) DO NOTHING`,[y(),t.siteId,t.namespaceId,t.personId,n,t.evidenceKind,i,t.priorPersonIds,t.priorIdentityNodeIds,t.candidateAssociationIds,JSON.stringify({...t.history,candidateTouches:t.candidateTouches}),s,t.occurredAt])}async function Le(e,t){let i=t.occurredAt??new Date().toISOString(),n=await at(e,t),s=L("promotion_evidence",`${t.evidenceKind}:${t.evidenceRef}`),a=await Li(e,{siteId:t.siteId,namespaceId:t.namespaceId,personId:t.personId,candidateEvidenceKind:t.candidateEvidenceKind,evidenceRefHmac:s,occurredAt:i});await Ai(e,{...t,history:n,candidateAssociationIds:a.associationIds,candidateTouches:a.touches,occurredAt:i})}async function Ii(e,t){let i=t.customerId||t.orderId||t.email||t.phone;if(!i||!await et(e,t.siteId,t.visitorId,t.sessionId))return null;let n=await st(e,t.siteId,"pixel"),s=[...t.email?[{kind:"email",valueHmac:L("email",t.email)}]:[],...t.phone?[{kind:"phone",valueHmac:L("phone",t.phone)}]:[],...t.customerId?[{kind:"customer_id",valueHmac:L("customer_id",t.customerId)}]:[],...t.orderId?[{kind:"customer_id",valueHmac:L("customer_id",`order:${t.orderId}`)}]:[]];await ie(e,{siteId:t.siteId,signals:s});let a=`XRay::${L("person_ref",i).slice(0,24)}`,r=[...s,...t.visitorId?[{kind:"visitor_id",valueHmac:L("visitor_id",t.visitorId)}]:[],...t.sessionId?[{kind:"session_id",valueHmac:L("session_id",t.sessionId)}]:[]],o=await Te(e,{siteId:t.siteId,namespaceId:n,crmPersonRef:a,signals:r}),d=o.personId,_=[];t.visitorId&&_.push({kind:"visitor_id",value:t.visitorId,evidence:"pixel_identify",confidence:.98}),t.sessionId&&_.push({kind:"session_id",value:t.sessionId,evidence:"pixel_identify",confidence:.98}),t.email&&_.push({kind:"email",value:t.email,evidence:"pixel_identify",confidence:1}),t.phone&&_.push({kind:"phone",value:t.phone,evidence:"pixel_identify",confidence:.99}),t.customerId&&_.push({kind:"customer_id",value:t.customerId,evidence:"customer_id",confidence:1}),t.orderId&&_.push({kind:"customer_id",value:`order:${t.orderId}`,evidence:"order_id",confidence:1});for(let[E,N]of Object.entries(z(t.clickIds)))_.push({kind:"click_id",value:`${E}:${N}`,evidence:E,confidence:.9});let u=[],c=!1;for(let E of _){let N=await e.query(`INSERT INTO analytics_identity_nodes(id, site_id, namespace_id, kind, value_hmac) VALUES ($1,$2,$3,$4,$5)
2389
+ ON CONFLICT(site_id, kind, value_hmac) DO UPDATE SET last_seen_at = now(),
2390
+ namespace_id=COALESCE(analytics_identity_nodes.namespace_id,EXCLUDED.namespace_id) RETURNING id`,[y(),t.siteId,n,E.kind,L(E.kind,E.value)]);u.push(N.rows[0].id),await e.query(`INSERT INTO analytics_identity_edges(id, site_id, namespace_id, person_id, identity_node_id, evidence_kind, confidence)
2391
+ VALUES ($1,$2,$3,$4,$5,$6,$7)
2392
+ ON CONFLICT(person_id, identity_node_id, evidence_kind) DO UPDATE
2393
+ SET last_seen_at = now(), confidence = greatest(analytics_identity_edges.confidence, EXCLUDED.confidence),
2394
+ namespace_id=COALESCE(analytics_identity_edges.namespace_id,EXCLUDED.namespace_id)`,[y(),t.siteId,n,d,N.rows[0].id,E.evidence,E.confidence]);let A=await J(e,{siteId:t.siteId,personId:d,identityNodeId:N.rows[0].id,evidenceKind:E.evidence});c||=A==="conflict"}return c||await Le(e,{siteId:t.siteId,namespaceId:n,personId:d,evidenceKind:t.customerId?"authenticated_login":"browser_identify",candidateEvidenceKind:t.email?"verified_email":t.phone?"verified_phone":"authenticated_user",evidenceRef:i,priorPersonIds:o.mergedPersonIds,priorIdentityNodeIds:u,visitorId:t.visitorId,sessionId:t.sessionId,occurredAt:t.occurredAt}),t.customerId&&!c&&await ne(e,{siteId:t.siteId,personId:d,consentSubjectRefs:[t.visitorId??"",t.sessionId??""],evidenceType:"authenticated_user",email:t.email,phone:t.phone}),d}async function Fn(e){let t=await l().connect();try{if(await t.query("BEGIN"),!await et(t,e.siteId,e.visitorId,e.sessionId))return await t.query("COMMIT"),{personId:null,linkedSignals:0,conflicts:0};let i=await st(t,e.siteId,"form"),n=[...e.email?[{kind:"email",valueHmac:L("email",e.email)}]:[],...e.phone?[{kind:"phone",valueHmac:L("phone",e.phone)}]:[]];await ie(t,{siteId:e.siteId,signals:n});let s=[...n,...e.visitorId?[{kind:"visitor_id",valueHmac:L("visitor_id",e.visitorId)}]:[],...e.sessionId?[{kind:"session_id",valueHmac:L("session_id",e.sessionId)}]:[]],a=await Te(t,{siteId:e.siteId,namespaceId:i,crmPersonRef:e.crmPersonRef,crmEntityId:e.crmEntityId,signals:s}),r=a.personId,o=[];e.visitorId&&o.push({kind:"visitor_id",value:e.visitorId,evidence:"first_party_form",confidence:.98}),e.sessionId&&o.push({kind:"session_id",value:e.sessionId,evidence:"first_party_form",confidence:.95}),e.email&&o.push({kind:"email",value:e.email,evidence:"submitted_email",confidence:1}),e.phone&&o.push({kind:"phone",value:e.phone,evidence:"submitted_phone",confidence:.99});for(let[u,c]of Object.entries(z(e.clickIds)))o.push({kind:"click_id",value:`${u}:${c}`,evidence:u,confidence:.9});let d=[],_=0;for(let u of o){let c=await t.query(`INSERT INTO analytics_identity_nodes(id, site_id, namespace_id, kind, value_hmac) VALUES ($1,$2,$3,$4,$5)
2395
+ ON CONFLICT(site_id, kind, value_hmac) DO UPDATE SET last_seen_at = now(),
2396
+ namespace_id=COALESCE(analytics_identity_nodes.namespace_id,EXCLUDED.namespace_id) RETURNING id`,[y(),e.siteId,i,u.kind,L(u.kind,u.value)]);d.push(c.rows[0].id),await t.query(`INSERT INTO analytics_identity_edges(id, site_id, namespace_id, person_id, identity_node_id, evidence_kind, confidence)
2397
+ VALUES ($1,$2,$3,$4,$5,$6,$7)
2398
+ ON CONFLICT(person_id, identity_node_id, evidence_kind) DO UPDATE SET last_seen_at = now(),
2399
+ confidence = greatest(analytics_identity_edges.confidence, EXCLUDED.confidence),
2400
+ namespace_id=COALESCE(analytics_identity_edges.namespace_id,EXCLUDED.namespace_id)`,[y(),e.siteId,i,r,c.rows[0].id,u.evidence,u.confidence]),await J(t,{siteId:e.siteId,personId:r,crmEntityId:e.crmEntityId,identityNodeId:c.rows[0].id,evidenceKind:u.evidence})==="conflict"&&(_+=1)}return e.submissionId&&await t.query("UPDATE analytics_form_submissions SET person_id = $1 WHERE id = $2 AND site_id = $3",[r,e.submissionId,e.siteId]),_===0&&await Le(t,{siteId:e.siteId,namespaceId:i,personId:r,evidenceKind:"form_fill",candidateEvidenceKind:e.email?"verified_email":e.phone?"verified_phone":"authenticated_user",evidenceRef:e.submissionId??e.crmEntityId??e.crmPersonRef??r,priorPersonIds:a.mergedPersonIds,priorIdentityNodeIds:d,visitorId:e.visitorId,sessionId:e.sessionId}),_===0&&await ne(t,{siteId:e.siteId,personId:r,consentSubjectRefs:[e.visitorId??""],evidenceType:e.evidenceType??"first_party_form",displayName:e.displayName,email:e.email,phone:e.phone}),await t.query("COMMIT"),{personId:r,linkedSignals:o.length,conflicts:_}}catch(i){throw await t.query("ROLLBACK"),i}finally{t.release()}}async function $n(e){let t=await l().connect();try{await t.query("BEGIN");let n=(await t.query(`SELECT p.id,p.public_ref,p.namespace_id FROM analytics_people p
2401
+ JOIN analytics_identity_namespaces n ON n.id=p.namespace_id AND n.site_id=p.site_id AND n.status='approved'
2402
+ WHERE p.site_id=$1 AND p.crm_person_ref=$2 LIMIT 1 FOR UPDATE OF p`,[e.siteId,e.crmPersonRef])).rows[0];if(!n||!await ti(t,e.siteId,n.public_ref))return await t.query("COMMIT"),{personId:null,linkedSignals:0,conflicts:0};let s=[...e.email?[{kind:"email",value:e.email,confidence:1}]:[],...e.phone?[{kind:"phone",value:e.phone,confidence:.99}]:[]];await ie(t,{siteId:e.siteId,signals:s.map(c=>({kind:c.kind,valueHmac:L(c.kind,c.value)}))});let a=await Te(t,{siteId:e.siteId,namespaceId:n.namespace_id,crmPersonRef:e.crmPersonRef,crmEntityId:e.crmEntityId,signals:s.map(c=>({kind:c.kind,valueHmac:L(c.kind,c.value)}))}),r=a.personId,d=(await t.query("SELECT public_ref FROM analytics_people WHERE id=$1 AND site_id=$2",[r,e.siteId])).rows[0]?.public_ref??n.public_ref,_=[],u=0;for(let c of s){let E=await t.query(`INSERT INTO analytics_identity_nodes(id,site_id,namespace_id,kind,value_hmac) VALUES($1,$2,$3,$4,$5)
2403
+ ON CONFLICT(site_id,kind,value_hmac) DO UPDATE SET last_seen_at=now(),
2404
+ namespace_id=COALESCE(analytics_identity_nodes.namespace_id,EXCLUDED.namespace_id) RETURNING id`,[y(),e.siteId,n.namespace_id,c.kind,L(c.kind,c.value)]);_.push(E.rows[0].id),await t.query(`INSERT INTO analytics_identity_edges(id,site_id,namespace_id,person_id,identity_node_id,evidence_kind,confidence)
2405
+ VALUES($1,$2,$3,$4,$5,'crm_contact',$6)
2406
+ ON CONFLICT(person_id,identity_node_id,evidence_kind) DO UPDATE SET last_seen_at=now(),
2407
+ confidence=greatest(analytics_identity_edges.confidence,EXCLUDED.confidence)`,[y(),e.siteId,n.namespace_id,r,E.rows[0].id,c.confidence]),await J(t,{siteId:e.siteId,personId:r,crmEntityId:e.crmEntityId,identityNodeId:E.rows[0].id,evidenceKind:"crm_contact"})==="conflict"&&(u+=1)}return u===0&&await Le(t,{siteId:e.siteId,namespaceId:n.namespace_id,personId:r,evidenceKind:"crm_identity",candidateEvidenceKind:"crm_contact",evidenceRef:e.crmPersonRef,priorPersonIds:a.mergedPersonIds,priorIdentityNodeIds:_}),u===0&&await ne(t,{siteId:e.siteId,personId:r,consentSubjectRefs:[d],evidenceType:"crm_contact",displayName:e.displayName,email:e.email,phone:e.phone}),await t.query("COMMIT"),{personId:r,linkedSignals:s.length,conflicts:u}}catch(i){throw await t.query("ROLLBACK"),i}finally{t.release()}}async function qn(e,t,i){let n=l();await g(n,e,t);let s=await n.query(`SELECT p.id, p.first_seen_at, p.last_seen_at, true AS crm_linked,
2408
+ (SELECT count(*)::int FROM analytics_identity_edges e WHERE e.person_id = p.id) AS signal_count,
2409
+ (SELECT count(*)::int FROM analytics_form_submissions f WHERE f.person_id = p.id) AS form_submission_count,
2410
+ (SELECT count(*)::int FROM analytics_conversions c WHERE c.person_id = p.id) AS conversion_count,
2411
+ (SELECT COALESCE(sum(c.value_minor), 0)::bigint FROM analytics_conversions c WHERE c.person_id = p.id) AS revenue_minor
2412
+ FROM analytics_people p WHERE p.site_id = $1 ORDER BY p.last_seen_at DESC, p.id DESC LIMIT 1000`,[e]),a=$(s.rows,i,F("people",e,{start:"all",end:"all"}));return{people:a.items,pageInfo:a.pageInfo}}async function Mn(e,t,i,n){let s=l();if(await g(s,e,i),!(await s.query("SELECT 1 FROM analytics_people WHERE id = $1 AND site_id = $2",[t,e])).rowCount)throw new m("analytics_person_not_found","Attributed person not found.",404);let r=await s.query(`SELECT * FROM (
2413
+ SELECT e.occurred_at, 'event'::text AS kind, e.event_name AS label, e.source, e.medium, e.campaign, e.path, 0::bigint AS value_minor, NULL::text AS currency
2414
+ FROM analytics_events e WHERE e.site_id = $1 AND e.person_id = $2
2415
+ UNION ALL
2416
+ SELECT f.occurred_at, 'form_submission', af.name, f.source, f.medium, f.campaign, NULL, 0, NULL
2417
+ FROM analytics_form_submissions f JOIN analytics_forms af ON af.id = f.form_id WHERE f.site_id = $1 AND f.person_id = $2
2418
+ UNION ALL
2419
+ SELECT c.occurred_at, 'conversion', c.conversion_kind, NULL, NULL, NULL, NULL, c.value_minor, c.currency
2420
+ FROM analytics_conversions c WHERE c.site_id = $1 AND c.person_id = $2
2421
+ ) journey ORDER BY occurred_at DESC LIMIT 1000`,[e,t]),o=$(r.rows,n,F("person_journey",e,{start:t,end:t}));return{personId:t,touchpoints:o.items,pageInfo:o.pageInfo}}async function kn(e){let t=l();await D(t,e.siteId,e.userId);let i=await t.connect(),n=e.id??y();try{if(await i.query("BEGIN"),!(await i.query(`INSERT INTO analytics_crm_imports(id, site_id, requested_by_user_id, source_system, filename, accepted_count, rejected_count)
2422
+ VALUES ($1,$2,$3,$4,$5,$6,$7)
2423
+ ON CONFLICT(id) DO NOTHING
2424
+ RETURNING id,status,accepted_count,rejected_count`,[n,e.siteId,e.userId,e.sourceSystem.slice(0,80),e.filename.slice(0,240),e.rows.length,e.rejectedCount])).rowCount){let r=(await i.query(`SELECT id,status,accepted_count,rejected_count
2425
+ FROM analytics_crm_imports
2426
+ WHERE id=$1 AND site_id=$2 AND requested_by_user_id=$3`,[n,e.siteId,e.userId])).rows[0];if(!r)throw new m("analytics_crm_import_idempotency_conflict","Import idempotency receipt belongs to another request.",409);return await i.query("COMMIT"),{id:r.id,status:r.status,acceptedCount:Number(r.accepted_count),rejectedCount:Number(r.rejected_count)}}for(let a of e.rows)await i.query(`INSERT INTO analytics_crm_import_rows(id, import_id, crm_person_ref, payload_ciphertext)
2427
+ VALUES ($1,$2,$3,$4) ON CONFLICT(import_id, crm_person_ref) DO NOTHING`,[y(),n,a.crmPersonRef,a.payloadCiphertext]);return await i.query("COMMIT"),{id:n,status:"queued",acceptedCount:e.rows.length,rejectedCount:e.rejectedCount}}catch(s){throw await i.query("ROLLBACK"),s}finally{i.release()}}async function Hn(e,t,i){let n=l();await g(n,e,t);let s=await n.query(`SELECT id, source_system, filename, status, accepted_count, rejected_count, created_at, completed_at
2428
+ FROM analytics_crm_imports WHERE site_id = $1 ORDER BY created_at DESC, id DESC LIMIT 1000`,[e]),a=$(s.rows,i,F("crm_imports",e,{start:"all",end:"all"}));return{imports:a.items,pageInfo:a.pageInfo}}async function Pn(e=10){return(await l().query(`WITH due AS (
2429
+ SELECT r.id FROM analytics_crm_import_rows r
2430
+ WHERE r.delivery_status='pending' AND r.next_attempt_at <= now()
2431
+ AND r.expires_at > now() AND (r.lease_until IS NULL OR r.lease_until < now())
2432
+ ORDER BY r.next_attempt_at, r.created_at FOR UPDATE SKIP LOCKED LIMIT $1
2433
+ )
2434
+ UPDATE analytics_crm_import_rows r SET attempts=r.attempts+1,
2435
+ lease_until=now()+interval '5 minutes'
2436
+ FROM due, analytics_crm_imports i WHERE r.id=due.id AND i.id=r.import_id
2437
+ RETURNING r.id, r.import_id, i.requested_by_user_id, r.payload_ciphertext, r.attempts`,[Math.min(Math.max(e,1),50)])).rows.map(i=>({id:i.id,importId:i.import_id,ownerUserId:Number(i.requested_by_user_id),payloadCiphertext:i.payload_ciphertext,attempts:i.attempts}))}async function jn(e,t){let n=await l().connect();try{await n.query("BEGIN"),await n.query(`UPDATE analytics_crm_import_rows SET delivery_status='delivered', delivered_at=now(),
2438
+ lease_until=NULL, last_error_code=NULL WHERE id=$1`,[e]),await n.query(`UPDATE analytics_crm_imports i SET status=CASE
2439
+ WHEN EXISTS(SELECT 1 FROM analytics_crm_import_rows r WHERE r.import_id=i.id AND r.delivery_status='pending') THEN 'processing'
2440
+ WHEN EXISTS(SELECT 1 FROM analytics_crm_import_rows r WHERE r.import_id=i.id AND r.delivery_status IN ('failed','expired')) THEN 'partial'
2441
+ ELSE 'complete' END,
2442
+ completed_at=CASE WHEN EXISTS(SELECT 1 FROM analytics_crm_import_rows r WHERE r.import_id=i.id AND r.delivery_status='pending') THEN NULL ELSE now() END
2443
+ WHERE i.id=$1`,[t]),await n.query("COMMIT")}catch(s){throw await n.query("ROLLBACK"),s}finally{n.release()}}async function Kn(e,t,i,n){let s=i>=8,a=Math.min(5*2**Math.max(0,i-1),360);await l().query(`UPDATE analytics_crm_import_rows SET
2444
+ delivery_status=CASE WHEN expires_at<=now() THEN 'expired' WHEN $2 THEN 'failed' ELSE 'pending' END,
2445
+ lease_until=NULL, last_error_code=$3,
2446
+ next_attempt_at=now()+($4::text || ' minutes')::interval WHERE id=$1`,[e,s,n.slice(0,80),String(a)]),s&&await l().query("UPDATE analytics_crm_imports SET status='partial' WHERE id=$1",[t])}async function Xn(e){le(e.eventMapping);let t=X(e.eventMapping),i=Ee(e.platform,e.externalDatasetId),n=e.operatingAccountId?.trim();if(e.platform==="google"&&!n)throw new m("analytics_provider_destination_required","A Google Ads operating account ID is required for Data Manager activation.",400);if(oe(e.validatedConnection,e.platform),e.validatedConnection.id!==e.connectionRef)throw new m("analytics_provider_action_unavailable","The selected connection cannot send conversions for this provider.",409);let s=l();return await D(s,e.siteId,e.userId),(await s.query(`INSERT INTO analytics_activation_destinations(id, site_id, platform, name, connection_ref, external_dataset_id, event_mapping, provider_config, mapping_version, automatic_delivery_enabled, created_by_user_id)
2447
+ VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb,$8::jsonb,$9,false,$10) RETURNING *`,[y(),e.siteId,e.platform,e.name.trim(),e.connectionRef.trim(),i,JSON.stringify(t),JSON.stringify(n?{operatingAccountId:n}:{}),t.schemaVersion,e.userId])).rows[0]}async function Bn(e,t){let i=l();return await g(i,e,t),(await i.query(`SELECT d.*, count(j.id)::int AS delivery_count,
2448
+ count(j.id) FILTER (WHERE j.status = 'pending')::int AS pending_count,
2449
+ count(j.id) FILTER (WHERE j.status = 'delivered')::int AS delivered_count,
2450
+ count(j.id) FILTER (WHERE j.status = 'failed')::int AS failed_count
2451
+ FROM analytics_activation_destinations d LEFT JOIN analytics_activation_jobs j ON j.destination_id = d.id
2452
+ WHERE d.site_id = $1 AND d.status <> 'archived' GROUP BY d.id ORDER BY d.created_at DESC`,[e])).rows}async function zn(e){let t=l();if(await D(t,e.siteId,e.userId),!(await t.query("UPDATE analytics_activation_destinations SET status = 'archived', updated_at = now() WHERE id = $1 AND site_id = $2",[e.destinationId,e.siteId])).rowCount)throw new m("analytics_activation_destination_not_found","Activation destination not found.",404)}function Ae(e,t){try{return Fe(e,t)}catch{return!1}}function gi(e){let t=e instanceof Error?e.message.toLowerCase():"";return/401|403|authori[sz]/.test(t)?"activation_authorization_failed":/429|rate.?limit/.test(t)?"activation_rate_limited":/invalid|required|schema|validation/.test(t)?"activation_payload_invalid":/timeout|timed out|temporar|unavailable|\b5\d\d\b/.test(t)?"activation_provider_unavailable":"activation_provider_test_failed"}function Ve(e){return e.warnings.slice(0,50).map(t=>t.slice(0,500))}async function Ie(e){let t=l();await D(t,e.siteId,e.userId);let n=(await t.query(`SELECT id, site_id, platform, connection_ref, external_dataset_id,
2453
+ event_mapping, provider_config, mapping_version, readiness, automatic_delivery_enabled
2454
+ FROM analytics_activation_destinations
2455
+ WHERE id=$1 AND site_id=$2 AND status='active'`,[e.destinationId,e.siteId])).rows[0];if(!n)throw new m("analytics_activation_destination_not_found","Activation destination not found.",404);return n}async function Yn(e){let t=await Ie(e);if(!t.connection_ref)throw new m("analytics_provider_action_unavailable","This destination has no connected provider account.",409);return t.connection_ref}async function rt(e){if(!(e.evidenceKind==="provider_test"&&e.nextReadiness==="verified"||e.evidenceKind==="real_delivery"&&e.nextReadiness==="live"||e.evidenceKind==="provider_diagnostic"&&e.nextReadiness==="degraded")||e.expectedReadiness.length===0)throw new m("analytics_activation_readiness_evidence_required","Provider evidence is required for this activation readiness transition.",409);let i=e.evidenceKind==="provider_test"?`EXISTS (SELECT 1 FROM analytics_activation_destination_tests t
2456
+ WHERE t.id=$5::uuid AND t.destination_id=d.id AND t.accepted=true)`:e.evidenceKind==="real_delivery"?`EXISTS (SELECT 1 FROM analytics_activation_delivery_attempts a
2457
+ WHERE a.id=$5::uuid AND a.destination_id=d.id AND a.accepted=true)`:`EXISTS (SELECT 1 FROM analytics_activation_destination_tests t
2458
+ WHERE t.id=$5::uuid AND t.destination_id=d.id AND t.safe_error_category IS NOT NULL)`,s=(await l().query(`UPDATE analytics_activation_destinations d
2459
+ SET readiness=$3, updated_at=now(),
2460
+ last_tested_at=CASE WHEN $3='verified' THEN now() ELSE last_tested_at END,
2461
+ last_receipt_id=CASE WHEN $3 IN ('verified','live') THEN $5::text ELSE last_receipt_id END
2462
+ WHERE d.id=$1 AND d.site_id=$2 AND d.readiness = ANY($4::text[])
2463
+ AND ${i}
2464
+ RETURNING d.readiness`,[e.destinationId,e.siteId,e.nextReadiness,[...e.expectedReadiness],e.evidenceId])).rows[0]?.readiness;if(!s)throw new m("analytics_activation_readiness_conflict","Activation readiness changed or the required provider evidence is unavailable.",409);return s}async function Wn(e){let t=await Ie(e);if(!t.connection_ref||!t.external_dataset_id)throw new m("analytics_provider_destination_required","This destination is missing its provider connection or destination identifier.",409);if(t.connection_ref!==e.validatedConnection.id)throw new m("analytics_provider_action_unavailable","The selected connection cannot test this destination.",409);let i=oe(e.validatedConnection,t.platform);if((t.platform==="meta"||t.platform==="tiktok")&&!e.testEventCode)throw new m("analytics_activation_provider_test_code_required",`A ${t.platform} provider test event code is required; X-Ray will not send a synthetic test as a live event.`,400);if(t.platform==="reddit"&&!e.testId)throw new m("analytics_activation_provider_test_code_required","A Reddit CAPI test ID is required; X-Ray will not send a synthetic test as a live event.",400);let n=y(),s=pe("sha256").update(`xray-provider-test:${n}`).digest("hex"),a=t.provider_config??{},r={eventId:`xray-test:${n}`,eventName:"xray_destination_test",eventTime:new Date().toISOString(),actionSource:"system_generated",match:{emailSha256:s},operatingAccountId:a.operatingAccountId,...e.testEventCode?{testEventCode:e.testEventCode}:{},...e.testId?{testId:e.testId}:{}},o=$e({platform:t.platform,destinationId:t.external_dataset_id,eventMapping:t.event_mapping,payload:r});await l().query(`INSERT INTO analytics_activation_destination_tests(
2465
+ id,destination_id,mapping_version,actor
2466
+ ) VALUES ($1,$2,$3,$4)`,[n,t.id,t.mapping_version,e.actor.slice(0,240)]);try{let d=await e.runAction({connectionRef:t.connection_ref,actionName:i.actionName,idempotencyKey:`xray-destination-test:${n}`,payload:o}),_=qe(t.platform,d,r.eventId),u=d&&typeof d=="object"?d:{},c=u.validationOnly===!0||u.schemaValidationOnly===!0,E=_.accepted||t.platform==="google"&&c;await l().query(`UPDATE analytics_activation_destination_tests
2467
+ SET provider_request_id=$2, provider_event_id=$3, warnings=$4::jsonb,
2468
+ response_received_at=now(), accepted=$5, validation_only=$6,
2469
+ next_diagnostic_at=CASE WHEN $7 THEN now()+interval '2 minutes' ELSE NULL END
2470
+ WHERE id=$1 AND response_received_at IS NULL`,[n,_.requestId??null,_.eventId??null,JSON.stringify(Ve(_)),E,c,!!(i.supportsDiagnostics&&_.requestId&&!c)]);let N=t.readiness;return E&&t.readiness!=="live"&&(N=await rt({siteId:e.siteId,destinationId:t.id,expectedReadiness:["configured_unverified","degraded","verified"],nextReadiness:"verified",evidenceKind:"provider_test",evidenceId:n})),{testId:n,accepted:E,validationOnly:c,readiness:N,providerRequestId:_.requestId,providerEventId:_.eventId,warnings:Ve(_)}}catch(d){if(d instanceof m&&d.code.startsWith("analytics_activation_readiness_"))throw d;let _=gi(d);throw await l().query(`UPDATE analytics_activation_destination_tests
2471
+ SET safe_error_category=$2, response_received_at=now()
2472
+ WHERE id=$1 AND response_received_at IS NULL`,[n,_]),new m(_,"The provider did not accept the destination test.",409)}}async function Gn(e){let t=l();await g(t,e.siteId,e.userId);let i=Math.min(Math.max(e.limit??50,1),100);return(await t.query(`SELECT * FROM (
2473
+ SELECT t.id, 'test'::text AS kind, t.mapping_version, t.provider_request_id,
2474
+ t.provider_event_id, t.warnings, t.safe_error_category,
2475
+ t.request_started_at, t.response_received_at, t.next_diagnostic_at,
2476
+ t.actor, t.accepted, t.validation_only, t.created_at,
2477
+ NULL::uuid AS job_id, false AS retryable
2478
+ FROM analytics_activation_destination_tests t
2479
+ JOIN analytics_activation_destinations d ON d.id=t.destination_id
2480
+ WHERE t.destination_id=$1 AND d.site_id=$2
2481
+ UNION ALL
2482
+ SELECT a.id, 'delivery'::text AS kind, a.mapping_version, a.provider_request_id,
2483
+ a.provider_event_id, a.warnings, a.safe_error_category,
2484
+ a.request_started_at, a.response_received_at, a.next_diagnostic_at,
2485
+ a.actor, a.accepted, false AS validation_only, a.created_at,
2486
+ CASE WHEN j.status IN ('failed','expired')
2487
+ AND d.status='active'
2488
+ AND d.readiness IN ('verified','live','degraded')
2489
+ THEN j.id ELSE NULL END AS job_id,
2490
+ (j.status IN ('failed','expired')
2491
+ AND d.status='active'
2492
+ AND d.readiness IN ('verified','live','degraded')) AS retryable
2493
+ FROM analytics_activation_delivery_attempts a
2494
+ JOIN analytics_activation_destinations d ON d.id=a.destination_id
2495
+ JOIN analytics_activation_jobs j ON j.id=a.job_id
2496
+ WHERE a.destination_id=$1 AND d.site_id=$2
2497
+ ) receipts ORDER BY created_at DESC LIMIT $3`,[e.destinationId,e.siteId,i])).rows.map(s=>{let a=s;if(a.kind==="delivery"&&a.retryable===!0&&typeof a.job_id=="string")return a;let r={...a};return delete r.job_id,{...r,retryable:!1}})}async function Vn(e){let t=l();await D(t,e.siteId,e.userId);let n=(await t.query(`UPDATE analytics_activation_jobs j
2498
+ SET status='pending', next_attempt_at=now(), lease_until=NULL,
2499
+ dead_lettered_at=NULL, last_error_code=NULL, response_category=NULL
2500
+ FROM analytics_activation_destinations d
2501
+ WHERE j.id=$1 AND d.id=j.destination_id AND d.site_id=$2
2502
+ AND d.status='active' AND d.automatic_delivery_enabled=true
2503
+ AND d.readiness IN ('verified','live','degraded')
2504
+ AND j.status IN ('failed','expired')
2505
+ RETURNING j.id, j.status, j.attempts, j.next_attempt_at`,[e.jobId,e.siteId])).rows[0];if(!n)throw new m("analytics_activation_retry_conflict","Only a failed delivery for an active verified destination can be retried.",409);return n}function vi(e){let t=e&&typeof e=="object"?e:{},i=Array.isArray(t.destinations)?t.destinations.filter(r=>!!(r&&typeof r=="object")):[],n=i.flatMap(r=>(Array.isArray(r.warnings)?r.warnings:[]).map(d=>{let _=d&&typeof d=="object"?d:{};return`${String(_.reason??"provider_warning").slice(0,200)}: ${String(_.count??0).slice(0,20)}`})).slice(0,50),s=i.some(r=>Array.isArray(r.errors)&&r.errors.length>0),a=i.some(r=>/pending|processing|received/i.test(String(r.status??"")));return{warnings:n,...s?{errorCategory:"activation_provider_diagnostic_rejected"}:{},pending:a}}async function Jn(e){let t=await Ie(e),i=oe(e.validatedConnection,t.platform);if(!i.supportsDiagnostics||!i.diagnosticsActionName||!t.connection_ref)throw new m("analytics_activation_diagnostics_unavailable","Diagnostics are not available for this activation provider.",409);if(t.connection_ref!==e.validatedConnection.id)throw new m("analytics_provider_action_unavailable","The selected connection cannot read diagnostics for this destination.",409);let s=(await l().query(`SELECT id, provider_request_id FROM analytics_activation_destination_tests
2506
+ WHERE destination_id=$1 AND provider_request_id IS NOT NULL
2507
+ ORDER BY created_at DESC LIMIT 1`,[t.id])).rows[0];if(!s)throw new m("analytics_activation_diagnostics_unavailable","No provider request is available for diagnostics.",409);let a=await e.runAction({connectionRef:t.connection_ref,actionName:i.diagnosticsActionName,idempotencyKey:`xray-diagnostics:${s.id}`,payload:{requestId:s.provider_request_id}}),r=vi(a);await l().query(`UPDATE analytics_activation_destination_tests
2508
+ SET warnings=$2::jsonb, safe_error_category=$3,
2509
+ next_diagnostic_at=CASE WHEN $4 THEN now()+interval '5 minutes' ELSE NULL END
2510
+ WHERE id=$1`,[s.id,JSON.stringify(r.warnings),r.errorCategory??null,r.pending]);let o=t.readiness;return r.errorCategory&&["verified","live"].includes(t.readiness)&&(o=await rt({siteId:e.siteId,destinationId:t.id,expectedReadiness:[t.readiness],nextReadiness:"degraded",evidenceKind:"provider_diagnostic",evidenceId:s.id})),{testId:s.id,readiness:o,...r}}async function Qn(e){let t=l(),i=await t.query(`SELECT id,event_mapping FROM analytics_activation_destinations
2511
+ WHERE site_id = $1 AND status = 'active' AND automatic_delivery_enabled=true
2512
+ AND readiness IN ('verified','live')`,[e.siteId]),n=0;for(let s of i.rows){if(!Ae(s.event_mapping,e.conversionKind))continue;let a=await t.query(`INSERT INTO analytics_activation_jobs(id, destination_id, conversion_id, person_id, payload_ciphertext)
2513
+ VALUES ($1,$2,$3,$4,$5) ON CONFLICT(destination_id, conversion_id) DO NOTHING`,[y(),s.id,e.conversionId,e.personId??null,e.payloadCiphertext]);n+=a.rowCount??0}return n}async function Zn(e){let t=y();return await l().query(`INSERT INTO analytics_form_delivery_jobs(id, submission_id, owner_user_id, payload_ciphertext, last_error_code)
2514
+ VALUES ($1,$2,$3,$4,$5)`,[t,e.submissionId,e.ownerUserId,e.payloadCiphertext,e.errorCode??null]),t}async function es(e){let t=await l().connect(),i=y(),n=y(),s=e.crmEntityId??y(),a=e.occurredAt??new Date().toISOString();try{await t.query("BEGIN");let r=await t.query(`INSERT INTO analytics_form_submissions(
2515
+ id,form_id,site_id,pixel_id,visitor_id,session_id,crm_person_ref,
2516
+ crm_entity_id,bridge_run_id,idempotency_key,source,medium,campaign,
2517
+ crm_delivery_status,click_ids,occurred_at
2518
+ ) VALUES($1,$2,$3,$4,$5,$6,$7,$8::uuid,$9,$10,$11,$12,$13,'pending',$14::jsonb,$15)
2519
+ ON CONFLICT(form_id,idempotency_key) WHERE idempotency_key IS NOT NULL DO NOTHING
2520
+ RETURNING id,bridge_run_id,crm_entity_id::text`,[i,e.form.id,e.form.site_id,e.form.pixel_id,e.visitorId??null,e.sessionId??null,e.legacyCrmPersonRef??null,s,n,e.idempotencyKey,e.source?.slice(0,180)??null,e.medium?.slice(0,180)??null,e.campaign?.slice(0,240)??null,JSON.stringify(z(e.clickIds)),a]),o=!!r.rows[0],d=r.rows[0]??(await t.query(`SELECT id,bridge_run_id::text,crm_entity_id::text
2521
+ FROM analytics_form_submissions
2522
+ WHERE form_id=$1 AND idempotency_key=$2 FOR UPDATE`,[e.form.id,e.idempotencyKey])).rows[0];if(!d?.bridge_run_id||!d.crm_entity_id)throw new m("form_bridge_idempotency_conflict","The form idempotency receipt belongs to a legacy submission.",409);let _=y(),u=await t.query(`INSERT INTO analytics_conversions(
2523
+ id,source_event_id,site_id,pixel_id,session_id,session_hmac,
2524
+ conversion_kind,value_minor,currency,occurred_at
2525
+ ) VALUES($1,$2,$3,$4,$5,$6,'form_submitted',0,'USD',$7)
2526
+ ON CONFLICT(site_id,source_event_id) DO UPDATE SET source_event_id=EXCLUDED.source_event_id
2527
+ RETURNING id`,[_,`form:${d.id}`,e.form.site_id,e.form.pixel_id,e.sessionId??null,e.sessionId?L("session_id",e.sessionId):null,a]);return await t.query(`INSERT INTO analytics_form_delivery_jobs(
2528
+ id,submission_id,owner_user_id,payload_ciphertext,bridge_run_id,
2529
+ crm_entity_id,idempotency_key
2530
+ ) VALUES($1,$2,$3,$4,$5,$6::uuid,$7)
2531
+ ON CONFLICT(submission_id) DO NOTHING`,[y(),d.id,e.ownerUserId,e.payloadCiphertext,d.bridge_run_id,d.crm_entity_id,e.idempotencyKey]),await t.query(`INSERT INTO analytics_form_bridge_receipts(
2532
+ id,bridge_run_id,submission_id,system,status,opaque_entity_id
2533
+ ) VALUES
2534
+ ($1,$2,$3,'analytics','delivered',NULL),
2535
+ ($4,$2,$3,'crm','pending',$5::uuid)
2536
+ ON CONFLICT(bridge_run_id,system) DO NOTHING`,[y(),d.bridge_run_id,d.id,y(),d.crm_entity_id]),await t.query("COMMIT"),{submissionId:d.id,bridgeRunId:d.bridge_run_id,crmEntityId:d.crm_entity_id,conversionId:u.rows[0].id,created:o}}catch(r){throw await t.query("ROLLBACK"),r}finally{t.release()}}async function ts(){let t=await l().connect();try{await t.query("BEGIN");let i=await t.query("DELETE FROM analytics_restricted_data WHERE expires_at <= now()"),n=await t.query(`UPDATE analytics_webhook_receipts SET payload_ciphertext=NULL
2537
+ WHERE payload_ciphertext IS NOT NULL AND payload_expires_at <= now()`),s=await t.query(`UPDATE analytics_crm_import_rows SET delivery_status='expired', lease_until=NULL,
2538
+ last_error_code='retention_expired'
2539
+ WHERE delivery_status='pending' AND expires_at <= now()`);return await t.query("COMMIT"),{restrictedDeleted:i.rowCount??0,webhookPayloadsCleared:n.rowCount??0,expiredImportRows:s.rowCount??0}}catch(i){throw await t.query("ROLLBACK"),i}finally{t.release()}}async function is(e=10){return(await l().query(`WITH due AS (
2540
+ SELECT id FROM analytics_form_delivery_jobs
2541
+ WHERE status = 'pending' AND next_attempt_at <= now() AND expires_at > now()
2542
+ ORDER BY next_attempt_at, created_at
2543
+ FOR UPDATE SKIP LOCKED LIMIT $1
2544
+ )
2545
+ UPDATE analytics_form_delivery_jobs j
2546
+ SET attempts = j.attempts + 1,
2547
+ next_attempt_at = now() + interval '5 minutes'
2548
+ FROM due WHERE j.id = due.id
2549
+ RETURNING j.id, j.submission_id, j.owner_user_id, j.payload_ciphertext, j.attempts,
2550
+ j.bridge_run_id::text,j.crm_entity_id::text,j.idempotency_key`,[Math.min(Math.max(e,1),50)])).rows.map(i=>({id:i.id,submissionId:i.submission_id,ownerUserId:Number(i.owner_user_id),payloadCiphertext:i.payload_ciphertext,attempts:i.attempts,bridgeRunId:i.bridge_run_id,crmEntityId:i.crm_entity_id,idempotencyKey:i.idempotency_key}))}async function ns(e,t){await Oi({jobId:e,submissionId:t})}async function Oi(e){let i=await l().connect();try{await i.query("BEGIN"),await i.query("UPDATE analytics_form_delivery_jobs SET status='delivered', delivered_at=now(), last_error_code=NULL WHERE id=$1",[e.jobId]),await i.query(`UPDATE analytics_form_submissions SET crm_delivery_status='delivered',
2551
+ crm_entity_id=COALESCE(crm_entity_id,$2::uuid) WHERE id=$1`,[e.submissionId,e.crmEntityId??null]),await i.query(`UPDATE analytics_form_bridge_receipts receipt
2552
+ SET status='delivered',opaque_entity_id=COALESCE(opaque_entity_id,$3::uuid),
2553
+ safe_error_code=NULL,updated_at=now()
2554
+ FROM analytics_form_delivery_jobs job
2555
+ WHERE job.id=$1 AND receipt.bridge_run_id=job.bridge_run_id
2556
+ AND receipt.submission_id=$2 AND receipt.system='crm'`,[e.jobId,e.submissionId,e.crmEntityId??null]),await i.query("COMMIT")}catch(n){throw await i.query("ROLLBACK"),n}finally{i.release()}}async function ss(e,t,i){let n=t>=8,s=Math.min(5*2**Math.max(0,t-1),360),a=await l().connect();try{await a.query("BEGIN");let o=(await a.query(`UPDATE analytics_form_delivery_jobs
2557
+ SET status = CASE WHEN expires_at <= now() THEN 'expired' WHEN $2 THEN 'failed' ELSE 'pending' END,
2558
+ last_error_code = $3,
2559
+ next_attempt_at = now() + ($4::text || ' minutes')::interval
2560
+ WHERE id = $1
2561
+ RETURNING bridge_run_id::text,submission_id::text,status`,[e,n,i.slice(0,80),String(s)])).rows[0];o?.bridge_run_id&&await a.query(`UPDATE analytics_form_bridge_receipts SET status=$3,
2562
+ safe_error_code=$4,updated_at=now()
2563
+ WHERE bridge_run_id=$1 AND submission_id=$2 AND system='crm'`,[o.bridge_run_id,o.submission_id,o.status,i.slice(0,80)]),await a.query("COMMIT")}catch(r){throw await a.query("ROLLBACK"),r}finally{a.release()}}async function as(e,t){if(!t)return!1;let i=null;try{i=new URL(t).hostname.toLowerCase()}catch{return!1}return!!(await l().query("SELECT 1 FROM analytics_pixel_domains WHERE pixel_id = $1 AND hostname = $2 AND state = 'approved'",[e,i])).rowCount}async function rs(e,t){let i=l(),n=await Qt(e,t),s=n.map(N=>N.last_event_at).filter(Boolean).sort().at(-1)??null,[a,r,o]=await Promise.all([i.query("SELECT max(occurred_at)::text AS last_conversion_at FROM analytics_conversions WHERE site_id = $1",[e]),i.query("SELECT max(refreshed_at)::text AS last_rollup_at FROM analytics_rollups_daily WHERE site_id = $1",[e]),i.query(`SELECT COALESCE(sum(accepted_count), 0)::int AS accepted,
2564
+ COALESCE(sum(rejected_count), 0)::int AS rejected
2565
+ FROM analytics_ingestion_receipts WHERE site_id = $1 AND created_at >= now() - interval '24 hours'`,[e])]),d=o.rows[0],_=d.accepted+d.rejected,u=_?d.rejected/_:0,c=!!(s&&Date.now()-Date.parse(s)>2880*60*1e3),E=n.some(N=>N.domains.some(A=>A.state==="observed_unverified"));return{status:n.length===0?"not_installed":E&&!s?"domain_mismatch":u>.25?"rejecting":c?"stale":s?"healthy":"waiting",pixels:n,lastEventAt:s,lastConversionAt:a.rows[0]?.last_conversion_at??null,lastRollupAt:r.rows[0]?.last_rollup_at??null,acceptedLast24Hours:d.accepted,rejectedLast24Hours:d.rejected,rejectionRate:u,checkedAt:new Date().toISOString()}}async function Si(e){let t=l(),i=y();await t.query("INSERT INTO analytics_rollup_runs(id, window_start, window_end, status) VALUES ($1, $2, $3, 'running')",[i,e.start,e.end]);let n=await t.connect();try{await n.query("BEGIN"),await n.query("DELETE FROM analytics_rollups_daily WHERE bucket >= $1::date AND bucket <= $2::date",[e.start,e.end]);let s=await n.query(`INSERT INTO analytics_rollups_daily(
2566
+ site_id, pixel_id, hostname, bucket, source, medium, campaign,
2567
+ pageviews, events, visitors, sessions, refreshed_at
2568
+ )
2569
+ SELECT site_id, pixel_id, hostname, occurred_at::date,
2570
+ COALESCE(source, '(direct)'), COALESCE(medium, '(none)'), COALESCE(campaign, '(none)'),
2571
+ count(*) FILTER (WHERE event_name = 'page_view')::int,
2572
+ count(*)::int,
2573
+ count(DISTINCT visitor_id)::int,
2574
+ count(DISTINCT session_id)::int,
2575
+ now()
2576
+ FROM analytics_events
2577
+ WHERE occurred_at >= $1::timestamptz AND occurred_at < $2::timestamptz
2578
+ GROUP BY 1, 2, 3, 4, 5, 6, 7
2579
+ ON CONFLICT (site_id, pixel_id, hostname, bucket, source, medium, campaign)
2580
+ DO UPDATE SET
2581
+ pageviews = EXCLUDED.pageviews,
2582
+ events = EXCLUDED.events,
2583
+ visitors = EXCLUDED.visitors,
2584
+ sessions = EXCLUDED.sessions,
2585
+ refreshed_at = EXCLUDED.refreshed_at`,[e.start,e.end]);await n.query("COMMIT");let a=s.rowCount??0;return await t.query("UPDATE analytics_rollup_runs SET status = 'complete', row_count = $2, completed_at = now() WHERE id = $1",[i,a]),{runId:i,rows:a}}catch(s){await n.query("ROLLBACK");let a=s?.code,r=s instanceof Error?s.message:String(s);throw console.error(JSON.stringify({event:"analytics_rollup_failed",runId:i,pgCode:a??null,message:r.slice(0,400)})),await t.query("UPDATE analytics_rollup_runs SET status = 'failed', completed_at = now(), error_code = $2 WHERE id = $1",[i,(a?`pg_${a}`:"rollup_failed").slice(0,60)]),s}finally{n.release()}}async function os(e=new Date){let i=await l().connect(),n=2026080501,s=!1;try{if(s=!!(await i.query("SELECT pg_try_advisory_lock($1) AS acquired",[n])).rows[0]?.acquired,!s)return{status:"already_running",rows:0};if((await i.query(`SELECT completed_at FROM analytics_rollup_runs
2586
+ WHERE status = 'complete' AND completed_at > $1::timestamptz
2587
+ ORDER BY completed_at DESC LIMIT 1`,[new Date(e.getTime()-55*6e4).toISOString()])).rows[0])return{status:"not_due",rows:0};let o=new Date(e),d=new Date(o.getTime()-4320*60*1e3);return{status:"complete",...await Si({start:d.toISOString(),end:o.toISOString()})}}finally{s&&await i.query("SELECT pg_advisory_unlock($1)",[n]).catch(()=>{}),i.release()}}function Je(e){let t=e==null?"":String(e),i=/^[=+\-@\t\r]/.test(t)?`'${t}`:t;return/[\n\r,\"]/.test(i)?`"${i.replaceAll('"','""')}"`:i}function Ci(e){let t=[...new Set(e.flatMap(i=>Object.keys(i)))];return t.length?[t.map(i=>Je(i)).join(","),...e.map(i=>t.map(n=>Je(i[n])).join(","))].join(`
2588
+ `):""}function ot(e){return e.split("_").map((t,i)=>i?t:t.charAt(0).toUpperCase()+t.slice(1)).join(" ")}function Ri(e){return e==null?"\u2014":String(e).replaceAll("|","\\|").replaceAll(`
2589
+ `," ")}function fi(e){let t=[...new Set(e.flatMap(i=>Object.keys(i)))];return t.length?[`| ${t.map(ot).join(" | ")} |`,`| ${t.map(()=>"---").join(" | ")} |`,...e.map(i=>`| ${t.map(n=>Ri(i[n])).join(" | ")} |`)]:["No rows matched this window and filter set."]}async function ds(e){let t=(await Jt(e.userId)).find(u=>u.id===e.siteId);if(!t)throw new m("analytics_site_not_found","Site not found.",404);let i=u=>{let c=u.format==="markdown"?"md":u.format;return{...u,mediaType:u.format==="csv"?"text/csv":u.format==="json"?"application/json":"text/markdown",filename:`${t.slug}-${u.report}.${c}`}};if(e.idempotencyKey){let c=(await l().query(`SELECT id, report, format, content FROM analytics_exports
2590
+ WHERE requested_by_user_id = $1 AND idempotency_key = $2`,[e.userId,e.idempotencyKey])).rows[0];if(c)return i(c)}let n=e.report==="overview"?await We(e.siteId,e.userId,e.filters):e.report==="acquisition"?await _i(e.siteId,e.userId,e.filters):e.report==="content"?await mi(e.siteId,e.userId,e.filters):await yi(e.siteId,e.userId,e.filters),s=e.report==="overview"?[n]:e.report==="acquisition"?n.channels:e.report==="content"?n.content:n.conversions,a;if(e.format==="json")a=JSON.stringify({version:1,site:{id:t.id,name:t.name},report:e.report,filters:e.filters,data:n},null,2);else if(e.format==="csv")a=Ci(s);else{let u=await We(e.siteId,e.userId,e.filters);a=[`# ${t.name} Site Analytics`,"",`Report: ${e.report}`,`Window: ${e.filters.start} to ${e.filters.end}`,`Fresh through: ${u.dataFreshThrough??"No accepted events"}`,"",`- Visitors: ${u.visitors}`,`- Sessions: ${u.sessions}`,`- Pageviews: ${u.pageviews}`,`- Verified conversions: ${u.conversions}`,`- Verified revenue: ${(u.revenueMinor/100).toFixed(2)}`,"",...e.report==="overview"?[]:[`## ${ot(e.report)} rows`,"",...fi(s),""],`Generated ${new Date().toISOString()} from the governed ${e.report} report contract.`].join(`
2591
+ `)}let r=y(),d=(await l().query(`INSERT INTO analytics_exports(id, site_id, requested_by_user_id, idempotency_key, report, format, filters, content)
2592
+ VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8)
2593
+ ON CONFLICT (requested_by_user_id, idempotency_key) WHERE idempotency_key IS NOT NULL DO NOTHING
2594
+ RETURNING id, report, format, content`,[r,e.siteId,e.userId,e.idempotencyKey??null,e.report,e.format,JSON.stringify(e.filters),a])).rows[0];if(d)return i(d);let _=await l().query(`SELECT id, report, format, content FROM analytics_exports
2595
+ WHERE requested_by_user_id = $1 AND idempotency_key = $2`,[e.userId,e.idempotencyKey]);if(!_.rows[0])throw new m("analytics_export_conflict","The export could not be created or replayed.",409);return i(_.rows[0])}export{De as a,Ue as b,bi as c,hi as d,xi as e,H as f,Mi as g,S as h,re as i,X as j,oe as k,Ft as l,qe as m,qt as n,me as o,Be as p,m as q,l as r,Vi as s,Ji as t,G as u,Qi as v,Zi as w,en as x,Jt as y,tn as z,nn as A,sn as B,an as C,rn as D,on as E,Qt as F,dn as G,cn as H,_n as I,un as J,En as K,ln as L,Ze as M,Zt as N,ei as O,Nn as P,ii as Q,ni as R,tt as S,V as T,oi as U,mn as V,yn as W,pn as X,Tn as Y,Ln as Z,An as _,We as $,In as aa,_i as ba,gn as ca,li as da,Ni as ea,mi as fa,vn as ga,yi as ha,On as ia,Sn as ja,Cn as ka,Rn as la,fn as ma,Dn as na,Un as oa,wn as pa,bn as qa,hn as ra,z as sa,L as ta,xn as ua,at as va,J as wa,Ii as xa,Fn as ya,$n as za,qn as Aa,Mn as Ba,kn as Ca,Hn as Da,Pn as Ea,jn as Fa,Kn as Ga,Xn as Ha,Bn as Ia,zn as Ja,Yn as Ka,rt as La,Wn as Ma,Gn as Na,Vn as Oa,Jn as Pa,Qn as Qa,Zn as Ra,es as Sa,ts as Ta,is as Ua,ns as Va,Oi as Wa,ss as Xa,as as Ya,rs as Za,Si as _a,os as $a,Je as ab,ds as bb};