@iann29/rastro 0.7.0 → 0.8.0

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 (44) hide show
  1. package/README.md +63 -0
  2. package/agent/integration.md +11 -0
  3. package/agent/manifest.json +4 -4
  4. package/dist/client/federation.d.ts +34 -9
  5. package/dist/client/federation.d.ts.map +1 -1
  6. package/dist/client/federation.js +14 -2
  7. package/dist/client/federation.js.map +1 -1
  8. package/dist/client/index.d.ts +350 -3
  9. package/dist/client/index.d.ts.map +1 -1
  10. package/dist/client/index.js +90 -5
  11. package/dist/client/index.js.map +1 -1
  12. package/dist/component/_generated/api.d.ts +2 -0
  13. package/dist/component/_generated/api.d.ts.map +1 -1
  14. package/dist/component/_generated/api.js.map +1 -1
  15. package/dist/component/_generated/component.d.ts +158 -0
  16. package/dist/component/_generated/component.d.ts.map +1 -1
  17. package/dist/component/constants.d.ts +5 -0
  18. package/dist/component/constants.d.ts.map +1 -1
  19. package/dist/component/constants.js +8 -0
  20. package/dist/component/constants.js.map +1 -1
  21. package/dist/component/leads.d.ts +190 -0
  22. package/dist/component/leads.d.ts.map +1 -0
  23. package/dist/component/leads.js +689 -0
  24. package/dist/component/leads.js.map +1 -0
  25. package/dist/component/schema.d.ts +119 -0
  26. package/dist/component/schema.js +39 -1
  27. package/dist/component/schema.js.map +1 -1
  28. package/dist/component/validators.d.ts +693 -0
  29. package/dist/component/validators.d.ts.map +1 -1
  30. package/dist/component/validators.js +129 -0
  31. package/dist/component/validators.js.map +1 -1
  32. package/dist/tracker/generated.d.ts +1 -1
  33. package/dist/tracker/generated.js +1 -1
  34. package/docs/federation-setup.md +29 -0
  35. package/docs/upgrading.md +30 -0
  36. package/llms.txt +2 -0
  37. package/package.json +1 -1
  38. package/src/component/_generated/api.ts +2 -0
  39. package/src/component/_generated/component.ts +181 -0
  40. package/src/component/constants.ts +8 -0
  41. package/src/component/leads.ts +823 -0
  42. package/src/component/schema.ts +45 -0
  43. package/src/component/validators.ts +164 -0
  44. package/src/tracker/generated.ts +1 -1
@@ -1,8 +1,11 @@
1
1
  import { defineSchema, defineTable } from "convex/server";
2
2
  import { v } from "convex/values";
3
3
  import {
4
+ adCatalogFieldsValidator,
4
5
  affiliateFieldsValidator,
5
6
  aggregateFieldsValidator,
7
+ leadEventTypeValidator,
8
+ leadFieldsValidator,
6
9
  batchedEventValidator,
7
10
  funnelFieldsValidator,
8
11
  goalFieldsValidator,
@@ -391,6 +394,48 @@ export default defineSchema({
391
394
  bots: v.number(),
392
395
  }).index("by_linkId_and_dayStart_and_shard", ["linkId", "dayStart", "shard"]),
393
396
 
397
+ // Leads the host's server reports (see leads.ts), one per host conversation.
398
+ leads: defineTable(leadFieldsValidator)
399
+ .index("by_siteId_and_leadKey", ["siteId", "leadKey"])
400
+ .index("by_siteId_and_firstAt", ["siteId", "firstAt"])
401
+ .index("by_siteId_and_ad_and_firstAt", ["siteId", "ad.adId", "firstAt"]),
402
+
403
+ // What happened to a lead, append-only; `eventId` makes a retry a no-op.
404
+ leadEvents: defineTable({
405
+ siteId: v.id("sites"),
406
+ leadId: v.id("leads"),
407
+ eventId: v.string(),
408
+ type: leadEventTypeValidator,
409
+ at: v.number(),
410
+ revenueCents: v.optional(v.number()),
411
+ })
412
+ .index("by_siteId_and_eventId", ["siteId", "eventId"])
413
+ .index("by_leadId_and_at", ["leadId", "at"]),
414
+
415
+ // One row per site, UTC cohort day, dimension and key. Lead volume is human
416
+ // scale (a conversation each), so a row is never a write hot spot.
417
+ leadDailyRollups: defineTable({
418
+ siteId: v.id("sites"),
419
+ dayStart: v.number(),
420
+ dimension: v.union(v.literal("origin"), v.literal("ad")),
421
+ key: v.string(),
422
+ leads: v.number(),
423
+ qualified: v.number(),
424
+ won: v.number(),
425
+ revenueCents: v.number(),
426
+ }).index("by_siteId_and_dimension_and_dayStart_and_key", [
427
+ "siteId",
428
+ "dimension",
429
+ "dayStart",
430
+ "key",
431
+ ]),
432
+
433
+ // Names the host fetches from the Meta Marketing API for the ads it saw.
434
+ adCatalog: defineTable(adCatalogFieldsValidator).index("by_siteId_and_adId", [
435
+ "siteId",
436
+ "adId",
437
+ ]),
438
+
394
439
  visitorSecrets: defineTable({
395
440
  key: v.literal("current"),
396
441
  value: v.string(),
@@ -373,6 +373,79 @@ export const trackedLinkFieldsValidator = v.object({
373
373
  disabledAt: v.optional(v.number()),
374
374
  });
375
375
 
376
+ /**
377
+ * Leads (0.8.0): a conversation the host's server reports, with no web session
378
+ * behind it. Today the channel is WhatsApp; the stage follows the host's
379
+ * pipeline as `lead` → `qualified` → `won`, or `lost`.
380
+ */
381
+ export const leadChannelValidator = v.literal("whatsapp");
382
+ export const leadStageValidator = v.union(
383
+ v.literal("lead"),
384
+ v.literal("qualified"),
385
+ v.literal("won"),
386
+ v.literal("lost"),
387
+ );
388
+ /** What the host may move a lead to; `open` undoes a sale or a loss. */
389
+ export const leadStageChangeValidator = v.union(
390
+ v.literal("qualified"),
391
+ v.literal("won"),
392
+ v.literal("lost"),
393
+ v.literal("open"),
394
+ );
395
+ export const leadEventTypeValidator = v.union(
396
+ v.literal("created"),
397
+ v.literal("qualified"),
398
+ v.literal("won"),
399
+ v.literal("lost"),
400
+ v.literal("open"),
401
+ );
402
+
403
+ /** The Meta Click-to-WhatsApp ad a lead came from, as the message carried it. */
404
+ export const leadAdValidator = v.object({
405
+ adId: v.optional(v.string()),
406
+ sourceApp: v.optional(v.string()),
407
+ sourceUrl: v.optional(v.string()),
408
+ ctwaClid: v.optional(v.string()),
409
+ title: v.optional(v.string()),
410
+ });
411
+
412
+ export const leadFieldsValidator = v.object({
413
+ siteId: v.id("sites"),
414
+ /** The host's opaque id for the lead (never a phone number). */
415
+ leadKey: v.string(),
416
+ channel: leadChannelValidator,
417
+ /** The channel's raw entry point, as the host received it. */
418
+ entryPoint: v.optional(v.string()),
419
+ /** The bucket reports group by: `ctwa_ad` for any ad, else the entry point or `none`. */
420
+ originKey: v.string(),
421
+ ad: v.optional(leadAdValidator),
422
+ /** A short display name the host chooses to share, if any. */
423
+ label: v.optional(v.string()),
424
+ /** The web visitor this lead came from, when a tracked link bridged them. */
425
+ visitorId: v.optional(v.string()),
426
+ stage: leadStageValidator,
427
+ revenueCents: v.number(),
428
+ firstAt: v.number(),
429
+ /** UTC day of `firstAt`: the cohort every later stage is counted in. */
430
+ dayStart: v.number(),
431
+ stageAt: v.number(),
432
+ qualifiedAt: v.optional(v.number()),
433
+ wonAt: v.optional(v.number()),
434
+ updatedAt: v.number(),
435
+ });
436
+
437
+ export const adCatalogFieldsValidator = v.object({
438
+ siteId: v.id("sites"),
439
+ adId: v.string(),
440
+ adName: v.optional(v.string()),
441
+ adsetId: v.optional(v.string()),
442
+ adsetName: v.optional(v.string()),
443
+ campaignId: v.optional(v.string()),
444
+ campaignName: v.optional(v.string()),
445
+ thumbnailUrl: v.optional(v.string()),
446
+ updatedAt: v.number(),
447
+ });
448
+
376
449
  export const ingestResultValidator = v.object({
377
450
  accepted: v.number(),
378
451
  duplicates: v.number(),
@@ -399,3 +472,94 @@ export type FunnelStep = Infer<typeof funnelStepValidator>;
399
472
  export type EventType = Infer<typeof eventTypeValidator>;
400
473
  export type Channel = Infer<typeof channelValidator>;
401
474
  export type Evidence = Infer<typeof evidenceValidator>;
475
+
476
+ // Lead reports. Ids are plain strings so the host's own functions can return
477
+ // these shapes unchanged across the component boundary.
478
+ const leadCountersFields = {
479
+ leads: v.number(),
480
+ qualified: v.number(),
481
+ won: v.number(),
482
+ revenueCents: v.number(),
483
+ };
484
+
485
+ export const leadAdRowValidator = v.object({
486
+ adId: v.string(),
487
+ adName: v.union(v.string(), v.null()),
488
+ sourceApp: v.union(v.string(), v.null()),
489
+ sourceUrl: v.union(v.string(), v.null()),
490
+ title: v.union(v.string(), v.null()),
491
+ thumbnailUrl: v.union(v.string(), v.null()),
492
+ ...leadCountersFields,
493
+ });
494
+
495
+ export const leadAdsetRowValidator = v.object({
496
+ adsetId: v.union(v.string(), v.null()),
497
+ adsetName: v.union(v.string(), v.null()),
498
+ ...leadCountersFields,
499
+ ads: v.array(leadAdRowValidator),
500
+ });
501
+
502
+ export const leadCampaignRowValidator = v.object({
503
+ campaignId: v.union(v.string(), v.null()),
504
+ campaignName: v.union(v.string(), v.null()),
505
+ ...leadCountersFields,
506
+ adsets: v.array(leadAdsetRowValidator),
507
+ });
508
+
509
+ export const leadsReportValidator = v.object({
510
+ from: v.number(),
511
+ to: v.number(),
512
+ /** The sites' currency, or null when they differ (revenue then mixes). */
513
+ currency: v.union(v.string(), v.null()),
514
+ totals: v.object(leadCountersFields),
515
+ origins: v.array(v.object({ key: v.string(), ...leadCountersFields })),
516
+ /**
517
+ * Campaign → ad set → ad, with ads the catalog does not name yet grouped
518
+ * under a null campaign and a null ad set.
519
+ */
520
+ campaigns: v.array(leadCampaignRowValidator),
521
+ });
522
+
523
+ export const leadSummaryValidator = v.object({
524
+ leadId: v.string(),
525
+ siteId: v.string(),
526
+ label: v.union(v.string(), v.null()),
527
+ channel: leadChannelValidator,
528
+ originKey: v.string(),
529
+ entryPoint: v.union(v.string(), v.null()),
530
+ adId: v.union(v.string(), v.null()),
531
+ sourceApp: v.union(v.string(), v.null()),
532
+ stage: leadStageValidator,
533
+ revenueCents: v.number(),
534
+ firstAt: v.number(),
535
+ stageAt: v.number(),
536
+ /** Whether a web journey is linked, through a tracked link. */
537
+ hasVisitor: v.boolean(),
538
+ });
539
+
540
+ export const leadJourneyValidator = v.union(
541
+ v.null(),
542
+ v.object({
543
+ lead: leadSummaryValidator,
544
+ ad: v.union(
545
+ v.null(),
546
+ v.object({
547
+ adId: v.union(v.string(), v.null()),
548
+ adName: v.union(v.string(), v.null()),
549
+ adsetName: v.union(v.string(), v.null()),
550
+ campaignName: v.union(v.string(), v.null()),
551
+ sourceApp: v.union(v.string(), v.null()),
552
+ sourceUrl: v.union(v.string(), v.null()),
553
+ title: v.union(v.string(), v.null()),
554
+ }),
555
+ ),
556
+ visitorId: v.union(v.string(), v.null()),
557
+ events: v.array(
558
+ v.object({
559
+ type: leadEventTypeValidator,
560
+ at: v.number(),
561
+ revenueCents: v.union(v.number(), v.null()),
562
+ }),
563
+ ),
564
+ }),
565
+ );
@@ -1,5 +1,5 @@
1
1
  // Generated by scripts/build-tracker.mjs. Do not edit.
2
- export const RASTRO_VERSION = "0.7.0";
2
+ export const RASTRO_VERSION = "0.8.0";
3
3
  export const TRACKER_SOURCE = "(()=>{const t=document.currentScript,e=t?.dataset.site,i=t?.dataset.endpoint||t?.src&&new URL(\"events\",t.src).href;if(!e||!i)return;const n=()=>crypto.randomUUID?.()||Math.random()+\"\",r=new URLSearchParams(location.search),s=r.get(\"ref\")?.slice(0,64);let o;for(const t of[\"source\",\"medium\",\"campaign\",\"term\",\"content\"]){const e=r.get(\"utm_\"+t)?.slice(0,64);e&&((o??={})[\"utm_\"+t]=e)}const a=\"gclid gbraid wbraid msclkid ttclid twclid li_fat_id ScCid rdt_cid dclid epik srsltid mc_cid mc_eid _hsenc igsh igshid fbclid\".split(\" \").filter(t=>r.has(t)).slice(0,8);let c,d,l,h,f=t.dataset.visitor||\"\",p=Boolean(f);try{f||=localStorage._rv||=n()}catch{}try{d=sessionStorage,l=d._r,c=d._ri==f&&d._rt,h=s||d._a,h&&(d._a=h),o||=d._u&&JSON.parse(d._u),o&&(d._u=JSON.stringify(o))}catch{h=s}let v,g=0;const u=[];let m=document.referrer.split(\"/\",3).join(\"/\")||void 0;const _=t=>{const n=u.splice(0,50);if(!n.length)return;const r=()=>new Blob([JSON.stringify({siteId:e,events:n,sentAt:Date.now()})]);let s=r();for(;s.size>64e3&&n.length>1;)u.unshift(n.pop()),s=r();t&&navigator.sendBeacon(i,s)||fetch(i,{method:\"POST\",body:s,keepalive:1}).catch(()=>{}),u.length&&_(t)},y=(t,e,i)=>{const r=Date.now(),s=r-c;c=r,s<18e5||(l=n(),\"pageview\"!=t&&(S=\"\",w()));try{d._r=l,d._ri=f,d._rt=r}catch{}u.push({eventId:`${l}.${r.toString(36)}.${g}`,sessionId:l,visitorId:f||l,type:t,name:e,path:location.pathname.slice(0,256),referrer:m,timestamp:r,sequence:g++,affiliateSlug:h,...i}),clearTimeout(v),v=setTimeout(_,1200)};let S;const w=()=>{const t=location.pathname;t!=S&&(S=t,y(\"pageview\",void 0,{properties:o,clid:a.length?a:void 0}))};window.rastro=(e,i,r)=>{if(\"context\"==e)return{sessionId:l,visitorId:f||l,identified:p};if(\"identify\"==e||\"reset\"==e){if(\"identify\"==e&&(!i||i.length>128||!/^[A-Za-z0-9][A-Za-z0-9_.:-]*$/.test(i)))return;if(\"identify\"==e&&i==f)return;_();const r=\"reset\"==e||p;if(p=\"identify\"==e,f=\"identify\"==e?i:n(),t.dataset.visitor=\"identify\"==e?f:\"\",r){h=void 0,o=void 0,m=void 0,a.length=0;try{\"reset\"==e&&(localStorage._rv=f),d._a=\"\",d._u=\"\"}catch{}}c=0,g=0,S=\"\",w()}\"event\"==e&&y(\"custom\",`${i||\"event\"}`.slice(0,80),r&&JSON.stringify(r).length>2e4?void 0:{properties:r})},document.addEventListener(\"click\",t=>{const e=t.target?.closest(\"a,button,[data-rastro-event]\");if(!e)return;const i=e.dataset.rastroEvent,n=e.href,r=n&&new URL(n),s=/^https?:$/.test(r?.protocol),o=s&&r.origin!=location.origin;y(i?\"custom\":o?\"outbound\":\"click\",i,{target:(e.dataset.rastroLabel||e.innerText||e.tagName).slice(0,64),href:s?((o?r.origin:\"\")+r.pathname).slice(0,256):void 0})},1);for(const t of[\"pushState\",\"replaceState\"]){const e=history[t];history[t]=(...t)=>{e.apply(history,t),w()}}addEventListener(\"popstate\",w);let b=0;const I=()=>{y(\"heartbeat\"),_()},L=()=>{b&&(clearInterval(b),b=0,y(\"leave\"),_(1))},E=()=>{document.hidden?L():b||(I(),b=setInterval(I,2e4))};addEventListener(\"pagehide\",L),addEventListener(\"pageshow\",E),document.addEventListener(\"visibilitychange\",E),b=setInterval(I,2e4),w(),E()})();";
4
4
  export const TRACKER_GZIP_BASE64 = "H4sIAAAAAAACA31W23LbOBL9FRmVYgHrNiI7l8pSi6gmO3nwludSq+RlXR4FIpsiNhTAAZrSaET++xRIUYrt1LyQALqBbjTOOQDnQr0/ZM4GmpDKXdZs0JLMGu/R0iLzpiZARXOZa9IBSQZDCObbEbR57YyltqW5DD5LEou7yef/3nGGW7QUGFAcF7L0WMxMwS+wbS+M8EiNt7MhulUxlczva3LSa5u7zefPtz/OJRdt+5Om8jjIxSVj4NUxxgK1z8pftdebwCuXaTLOytCPCgjKyzUSZx4LJuYyVCZDPoW3r8WsQpq4WeE8P25/4op7FlzjM2TANpibZsOAZXpTa7O2DBih70ecJbTEHsSxcjiGaWizZJf0JBImCeduPleHTtyPPg8KRTdM14qts8rkk/XKa5NPdsNvE7Lqq8knRL2Rdv2vMstC09Lkk0X2b5NPfE7LzOSTvLdibb5Ogg8VxQWy3rLJlmjyybIMaLOJWYey/5h8UqziJCZDXRnibMKELExF6Dmp916WOnAS4rSXd0PRMsihghIKRScMbE0w5HzbMga1+uBchdryQszI7w9F26p4NNWCnNdrlEu/bVtluegyTVl56KJXrgKGYJw9ekGlcrn0kPU/o1SRJLFFUKrQtrlcaiiThMeGKgW4to2eTZL8Z/HLz7LWPmA0NgLc4Nao3hLIG7s2xZ47MWZQqtDFvW1hraZHRDbq/qHf8OZMDI8Feo9+LNlLBq+E/L8zNrZF226dySfjCktFJ3ZZ1fST+kq+mYqeB1ZWaNdUPqaC76kQAf6hcit+/yTrQ6TgbZ4iDPRKLQS09AOlP2pCad2Oi048DIcVlOeiR/ksyGD+xPdvX+OrJBkjv7+eiUY2NpSmIG5l7WoueuJwMaMksXpr1pqclwFt/gF15iw3EETbFkhZyQ0cNkily1P26y+LTwxWLt+nAb4i1royW0yvOyH7KvNebDoBzTF4kiw5iQ72ihMgmLMWeXXeS0zmKptlykP41/U7fNO2vIroAVbrNW4N7tiFoiThC8UY7LgQA+4iWlQFA3yK/k/Kj5hrZN2Ekh/6It7m6ZcXh6qTLw5eklv0xeav3oo4su6+wBGbt3lawRHst3latG0FtK8xJbB6gylCralMT0IUe9Fw4tDNm7cCRhSlGyCzwUB6U6ceAv7eoM0wXV9egi4KUxlNuKiadVqClNJ0ArIKtf9kNuga4lsBWxWQxv4Srm+mU9H1J784ommnHmn8s9RmdKEWffEI9vxcUhigDIfauxo9GQypgygYqT6e31yng1MnRDfbGZu7nfQ6kHeKIxjwMbIp+CCZfxBTCo9YP/xNRU2OlkxhME/rLhKFHUf2cYG2ZR4DDosdnlqThF+YtjUnfN+8a9uLl7/d/3D1P3315/Tqnw/n5lKmVw//ePFSEgbiRoiRiM9XjQI0WpdcnKh6zqVt6zivVo9mQvG4PzdphO4z6XziVaTxlhOHUh2PwY2NzdgYT0FNe7SfE0kS/lRuVSGgF0rGoFdCxkYedJmaRtWDkT3dcG0PK+05y5pAbsPgy4uDadujsftyvhamAvxRdc8q5cV4Ajf4ej6knH6LJd+JDk7CqvP8Y1z4zgRCi56zrDLZVwZnBUVFkrRfI81lVrkQj4xpWDVEzsJ9rOfVAL6rPscHNogsPlZXo/BU+8G9DwxWYf9AiW+L8xPGRvl5+VtJVId5OiLFz2XtHbnMVQKcCknipfNmbezFmWDDwGzPzXysYermzDW0co3NWTpu0cBh2FfKn6Z2p1dYtS1KYy36T/gHxQ7p9c96g+LbVwbE3NMwjy+NMZmUMXHpT0wXj1ToRN0OrsWzd1DUxgVpik8hj3WlMxy63zx6ShPI+f09PczOTcWllBR5j1LXdbXnRxuQ6NHVPT/q2tVhiLUbbq3V6RK+HdRrz1mJ2tMKNTEBSy46uBtMqyThvSreWkK/1RVfCVipaRSzCvUWe/9rITr4OMw4ga40eY52fsdFumpbfsvjxIB0WukWbvB11Lbv5KzXWJocGdwJ+L45lG7H4KP4O5RH9q9MZWifldqusff/XhKxdvAx3uxczP4CGKXLCLcLAAA=";
5
5
  export const TRACKER_RAW_BYTES = 2999;