@koda-sl/baker-cli 0.316.1 → 0.320.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.
package/dist/cli.js CHANGED
@@ -13271,6 +13271,172 @@ var STEP_SUBMIT_TRIGGERS = Object.fromEntries(
13271
13271
  );
13272
13272
  var MAX_CONVERSION_NAME = 80;
13273
13273
 
13274
+ // ../api/src/event-ingest/matching.ts
13275
+ var MATCH_FIELDS = [
13276
+ "first_name",
13277
+ "last_name",
13278
+ "date_of_birth",
13279
+ "gender",
13280
+ "city",
13281
+ "region",
13282
+ "postal_code",
13283
+ "country"
13284
+ ];
13285
+ var MATCH_COOKIES = ["fbp", "fbc"];
13286
+ function tightened(value) {
13287
+ const stripped = value.toLowerCase().replace(/[^\p{L}\p{N}]/gu, "");
13288
+ return stripped === "" ? null : stripped;
13289
+ }
13290
+ function loosened(value) {
13291
+ const collapsed = value.trim().toLowerCase().replace(/\s+/g, " ");
13292
+ return collapsed === "" ? null : collapsed;
13293
+ }
13294
+ function birthDate(value) {
13295
+ const digits = value.replace(/\D/g, "");
13296
+ if (!/^\d{8}$/.test(digits)) return null;
13297
+ const month = Number(digits.slice(4, 6));
13298
+ const day = Number(digits.slice(6, 8));
13299
+ if (month < 1 || month > 12 || day < 1 || day > 31) return null;
13300
+ return digits;
13301
+ }
13302
+ function genderLetter(value) {
13303
+ const first = value.trim().toLowerCase().charAt(0);
13304
+ return first === "f" || first === "m" ? first : null;
13305
+ }
13306
+ function countryCode(value) {
13307
+ const code = value.trim().toLowerCase();
13308
+ return /^[a-z]{2}$/.test(code) ? code : null;
13309
+ }
13310
+ var MATCH_SPECS = {
13311
+ first_name: { keep: "hashed", normalize: tightened, meta: "fn", google: "hashed_first_name" },
13312
+ last_name: { keep: "hashed", normalize: tightened, meta: "ln", google: "hashed_last_name" },
13313
+ // Meta only. Google's enhanced conversions have no date of birth, so there is
13314
+ // no destination wanting it in the clear and every reason not to keep one.
13315
+ date_of_birth: { keep: "hashed", normalize: birthDate, meta: "db", google: null },
13316
+ gender: { keep: "plain", normalize: genderLetter, meta: "ge", google: null },
13317
+ city: { keep: "plain", normalize: loosened, meta: "ct", google: "city" },
13318
+ region: { keep: "plain", normalize: loosened, meta: "st", google: "state" },
13319
+ postal_code: { keep: "plain", normalize: loosened, meta: "zp", google: "postal_code" },
13320
+ country: { keep: "plain", normalize: countryCode, meta: "country", google: "country_code" }
13321
+ };
13322
+ var MATCH_FIELD_FORWARDING = [
13323
+ { field: "email", meta: "em", google: "hashed_email", hashed: true },
13324
+ { field: "phone", meta: "ph", google: "hashed_phone_number", hashed: true },
13325
+ // Kept in the clear — see `resolveOwnId`. The digest is still computed and
13326
+ // still stored beside it, so a Meta forward has one; what changed is that the
13327
+ // sender's own key is now readable, which is the entire reason they sent it.
13328
+ { field: "external_id", meta: "external_id", google: null, hashed: false },
13329
+ ...MATCH_FIELDS.map((field) => ({
13330
+ field,
13331
+ meta: MATCH_SPECS[field].meta,
13332
+ google: MATCH_SPECS[field].google,
13333
+ hashed: MATCH_SPECS[field].keep === "hashed"
13334
+ })),
13335
+ { field: "fbp", meta: "fbp", google: null, hashed: false },
13336
+ { field: "fbc", meta: "fbc", google: null, hashed: false }
13337
+ ];
13338
+
13339
+ // ../api/src/event-ingest/vocabulary.ts
13340
+ var INGEST_CLICK_ID_PARAMS = [
13341
+ "gclid",
13342
+ "wbraid",
13343
+ "gbraid",
13344
+ "dclid",
13345
+ "fbclid",
13346
+ "ttclid",
13347
+ "twclid",
13348
+ "li_fat_id",
13349
+ "msclkid",
13350
+ "rdt_cid",
13351
+ "epik",
13352
+ "sccid"
13353
+ ];
13354
+
13355
+ // ../api/src/event-sources/fields.ts
13356
+ var MATCH_LABELS = {
13357
+ first_name: "First name",
13358
+ last_name: "Last name",
13359
+ date_of_birth: "Date of birth",
13360
+ gender: "Gender",
13361
+ city: "City",
13362
+ region: "Region",
13363
+ postal_code: "Postcode",
13364
+ country: "Country"
13365
+ };
13366
+ var CLICK_LABELS = {
13367
+ gclid: "Google click id",
13368
+ wbraid: "Google click id (iOS, web to app)",
13369
+ gbraid: "Google click id (iOS, app)",
13370
+ dclid: "Google click id (Display & Video)",
13371
+ fbclid: "Meta click id",
13372
+ ttclid: "TikTok click id",
13373
+ twclid: "X click id",
13374
+ li_fat_id: "LinkedIn click id",
13375
+ msclkid: "Microsoft click id",
13376
+ rdt_cid: "Reddit click id",
13377
+ epik: "Pinterest click id",
13378
+ sccid: "Snapchat click id"
13379
+ };
13380
+ var SOURCE_FIELDS = [
13381
+ { field: "event_name", label: "What happened", group: "event" },
13382
+ { field: "event_time", label: "When it happened", group: "event" },
13383
+ { field: "event_id", label: "Its id", group: "event" },
13384
+ { field: "email", label: "Email", group: "person" },
13385
+ { field: "phone", label: "Phone", group: "person" },
13386
+ { field: "external_id", label: "Your own id for the person", group: "person" },
13387
+ ...MATCH_FIELDS.map((field) => ({ field, label: MATCH_LABELS[field], group: "person" })),
13388
+ { field: "value", label: "Amount", group: "money" },
13389
+ { field: "currency", label: "Currency", group: "money" },
13390
+ ...INGEST_CLICK_ID_PARAMS.map((field) => ({ field, label: CLICK_LABELS[field], group: "click" }))
13391
+ ];
13392
+ var SOURCE_FIELD_NAMES = SOURCE_FIELDS.map((spec) => spec.field);
13393
+ var FIELD_SET = new Set(SOURCE_FIELD_NAMES);
13394
+ function isSourceField(value) {
13395
+ return FIELD_SET.has(value);
13396
+ }
13397
+ var PROPERTY_ROW_PREFIX = "properties.";
13398
+ var PROPERTY_KEY_MAX = 64;
13399
+ var PROPERTY_KEY = /^[A-Za-z0-9_][A-Za-z0-9_.-]*$/;
13400
+ var RESERVED_PROPERTY_KEYS = ["baker_source", "hubspot_property", "hubspot_value"];
13401
+ function isPropertyRow(row) {
13402
+ return row.startsWith(PROPERTY_ROW_PREFIX);
13403
+ }
13404
+ function propertyKeyOf(row) {
13405
+ return isPropertyRow(row) ? row.slice(PROPERTY_ROW_PREFIX.length) : null;
13406
+ }
13407
+ function propertyKeyProblem(key) {
13408
+ const trimmed = key.trim();
13409
+ if (trimmed === "") return "A property needs a name.";
13410
+ if (trimmed.length > PROPERTY_KEY_MAX) return `A property name is at most ${PROPERTY_KEY_MAX} characters.`;
13411
+ if (!PROPERTY_KEY.test(trimmed)) {
13412
+ return `"${trimmed}" is not a property name: letters, digits, underscore, dash and dot only, starting with a letter or digit.`;
13413
+ }
13414
+ if (RESERVED_PROPERTY_KEYS.includes(trimmed)) return `"${trimmed}" is written by Baker itself; pick another name.`;
13415
+ return null;
13416
+ }
13417
+ function mappingRowProblem(row) {
13418
+ if (isSourceField(row)) return null;
13419
+ const key = propertyKeyOf(row);
13420
+ if (key === null) {
13421
+ return `"${row}" is not a field an event can carry. A field is one of ${SOURCE_FIELD_NAMES.join(", ")}; an extra property is spelled properties.<name>.`;
13422
+ }
13423
+ return propertyKeyProblem(key);
13424
+ }
13425
+
13426
+ // ../api/src/event-sources/template.ts
13427
+ var MENTION = /\{\{\s*([^{}]+?)\s*\}\}/g;
13428
+ function mentionOf(path48) {
13429
+ return `{{${path48}}}`;
13430
+ }
13431
+ function templateMentions(template) {
13432
+ const seen = [];
13433
+ for (const match of template.matchAll(MENTION)) {
13434
+ const path48 = (match[1] ?? "").trim();
13435
+ if (path48 !== "" && !seen.includes(path48)) seen.push(path48);
13436
+ }
13437
+ return seen;
13438
+ }
13439
+
13274
13440
  // ../api/src/analytics/eventKey.ts
13275
13441
  var SEP = ":";
13276
13442
  function formEventKey(flowSlug, nodeId, triggerId) {
@@ -13305,6 +13471,106 @@ function parseEventKey(key) {
13305
13471
  }
13306
13472
  }
13307
13473
 
13474
+ // ../api/src/analytics/sends.ts
13475
+ var SEND_PLATFORMS = ["meta", "googleAds", "linkedinInsightTag", "twitterAds", "googleAnalytics"];
13476
+ function isSendPlatform(value) {
13477
+ return SEND_PLATFORMS.includes(value);
13478
+ }
13479
+ var SEND_PLATFORM_SPECS = {
13480
+ meta: {
13481
+ title: "Meta",
13482
+ eventMeans: "A Meta event name \u2014 Lead, Purchase, Schedule, or one of your own",
13483
+ eventKind: "name",
13484
+ credentialFields: ["accessToken", "datasetId"],
13485
+ setupHint: "Add the Conversions API access token and dataset id to the Meta pixel in Tracking"
13486
+ },
13487
+ googleAds: {
13488
+ title: "Google Ads",
13489
+ eventMeans: "One of the account's conversion actions, picked from its list",
13490
+ eventKind: "id",
13491
+ credentialFields: ["oauthProviderId"],
13492
+ setupHint: "Connect the Google Ads conversion tag to a Google Ads account in Tracking"
13493
+ },
13494
+ linkedinInsightTag: {
13495
+ title: "LinkedIn",
13496
+ eventMeans: "The id of a conversion rule in LinkedIn Campaign Manager",
13497
+ eventKind: "id",
13498
+ credentialFields: ["oauthProviderId"],
13499
+ setupHint: "Connect the LinkedIn Insight Tag to a LinkedIn account in Tracking"
13500
+ },
13501
+ twitterAds: {
13502
+ title: "X",
13503
+ eventMeans: "The event tag id from X Events Manager, like tw-oka17-abc12",
13504
+ eventKind: "id",
13505
+ credentialFields: ["oauthProviderId"],
13506
+ setupHint: "Connect the X pixel to an X Ads account in Tracking"
13507
+ },
13508
+ googleAnalytics: {
13509
+ title: "Google Analytics",
13510
+ eventMeans: "A GA4 event name \u2014 generate_lead, purchase, or one of your own",
13511
+ eventKind: "name",
13512
+ credentialFields: ["apiSecret"],
13513
+ setupHint: "Add the Measurement Protocol API secret to the Google Analytics tag in Tracking"
13514
+ }
13515
+ };
13516
+ var SEND_ORIGINS = ["server", "website", "any"];
13517
+ var MAX_SEND_ID = 64;
13518
+ var MAX_SEND_EVENT_KEY = 320;
13519
+ function sendEventIsTemplated(event) {
13520
+ return templateMentions(event).length > 0;
13521
+ }
13522
+ function sendEventProblem(platform, event) {
13523
+ if (!isSendPlatform(platform)) return null;
13524
+ if (!sendEventIsTemplated(event)) return null;
13525
+ const spec = SEND_PLATFORM_SPECS[platform];
13526
+ if (spec.eventKind === "name") return null;
13527
+ return `${spec.title} identifies a conversion by an id it issued, so it cannot be written from the event. Paste the id; pass what varies as an extra parameter instead.`;
13528
+ }
13529
+
13530
+ // ../api/src/analytics/destinations.ts
13531
+ var CONVERSION_SEND_PREFIX = "conversionSend:";
13532
+ function isConversionSend(type) {
13533
+ return type.startsWith(CONVERSION_SEND_PREFIX);
13534
+ }
13535
+ var SERVER_SIDE_SENDER_TAG = {
13536
+ metaCapi: "meta",
13537
+ googleAdsCapi: "googleAds",
13538
+ linkedinCapi: "linkedinInsightTag",
13539
+ twitterAdsCapi: "twitterAds",
13540
+ ga4Mp: "googleAnalytics"
13541
+ };
13542
+ var DESTINATION_NAMES = {
13543
+ email: "Email",
13544
+ httpWebhook: "HTTP Webhook",
13545
+ zapier: "Zapier",
13546
+ tiktok: "TikTok Pixel",
13547
+ pinterest: "Pinterest Tag",
13548
+ googleTagManager: "Google Tag Manager",
13549
+ amplitude: "Amplitude",
13550
+ hotjar: "Hotjar",
13551
+ clarity: "Microsoft Clarity",
13552
+ hubspot: "HubSpot Events",
13553
+ vwo: "VWO",
13554
+ posthog: "PostHog",
13555
+ datafast: "DataFast",
13556
+ code: "Custom Code",
13557
+ pipedrive: "Pipedrive",
13558
+ googleSpreadsheet: "Google Sheets",
13559
+ hubspotForm: "HubSpot Form",
13560
+ goHighlevelContact: "GoHighLevel",
13561
+ crmble: "Crmble"
13562
+ };
13563
+ function destinationKey(type) {
13564
+ const platform = isConversionSend(type) ? type.slice(CONVERSION_SEND_PREFIX.length) : type;
13565
+ return SERVER_SIDE_SENDER_TAG[platform] ?? platform;
13566
+ }
13567
+ function destinationName(type) {
13568
+ if (type === "") return "";
13569
+ const key = destinationKey(type);
13570
+ if (isSendPlatform(key)) return SEND_PLATFORM_SPECS[key].title;
13571
+ return DESTINATION_NAMES[key] ?? type;
13572
+ }
13573
+
13308
13574
  // ../api/src/analytics/events.ts
13309
13575
  import { z as z28 } from "zod";
13310
13576
 
@@ -13464,11 +13730,15 @@ var analyticsBeaconClickIdsSchema = z28.object({
13464
13730
  gclid: z28.string().max(255).optional(),
13465
13731
  wbraid: z28.string().max(255).optional(),
13466
13732
  gbraid: z28.string().max(255).optional(),
13733
+ dclid: z28.string().max(255).optional(),
13467
13734
  fbclid: z28.string().max(255).optional(),
13468
13735
  ttclid: z28.string().max(255).optional(),
13469
13736
  twclid: z28.string().max(255).optional(),
13470
13737
  liFatId: z28.string().max(255).optional(),
13471
- msclkid: z28.string().max(255).optional()
13738
+ msclkid: z28.string().max(255).optional(),
13739
+ rdtCid: z28.string().max(255).optional(),
13740
+ epik: z28.string().max(255).optional(),
13741
+ sccid: z28.string().max(255).optional()
13472
13742
  });
13473
13743
  var analyticsBeaconIdentitySchema = z28.object({
13474
13744
  /** Lowercase hex SHA-256. Anything else is dropped rather than stored. */
@@ -13624,218 +13894,169 @@ var analyticsBeaconPayloadSchema = z28.object({
13624
13894
  consent: z28.object({ analytics: z28.boolean() }).optional()
13625
13895
  });
13626
13896
 
13627
- // ../api/src/event-ingest/matching.ts
13628
- var MATCH_FIELDS = [
13629
- "first_name",
13630
- "last_name",
13631
- "date_of_birth",
13632
- "gender",
13633
- "city",
13634
- "region",
13635
- "postal_code",
13636
- "country"
13637
- ];
13638
- var MATCH_COOKIES = ["fbp", "fbc"];
13639
- function tightened(value) {
13640
- const stripped = value.toLowerCase().replace(/[^\p{L}\p{N}]/gu, "");
13641
- return stripped === "" ? null : stripped;
13642
- }
13643
- function loosened(value) {
13644
- const collapsed = value.trim().toLowerCase().replace(/\s+/g, " ");
13645
- return collapsed === "" ? null : collapsed;
13646
- }
13647
- function birthDate(value) {
13648
- const digits = value.replace(/\D/g, "");
13649
- if (!/^\d{8}$/.test(digits)) return null;
13650
- const month = Number(digits.slice(4, 6));
13651
- const day = Number(digits.slice(6, 8));
13652
- if (month < 1 || month > 12 || day < 1 || day > 31) return null;
13653
- return digits;
13897
+ // ../api/src/analytics/triggers.ts
13898
+ var TRIGGERS = {
13899
+ "calendlyEvent.eventScheduled": {
13900
+ what: "Call booked",
13901
+ meaning: "Somebody picked a slot and confirmed it. This is the booking."
13902
+ },
13903
+ "calendlyEvent.dateAndTimeSelected": {
13904
+ what: "Time picked",
13905
+ meaning: "Somebody chose a slot but has not confirmed the booking yet."
13906
+ },
13907
+ "calendlyEvent.eventTypeViewed": {
13908
+ what: "Calendar opened",
13909
+ meaning: "The calendar appeared. Nobody has picked a time yet.",
13910
+ lifecycle: true
13911
+ },
13912
+ "calendlyEvent.profilePageViewed": {
13913
+ what: "Booking page shown",
13914
+ meaning: "Calendly's own page appeared. Nobody has picked a time yet.",
13915
+ lifecycle: true
13916
+ },
13917
+ "customForm.onFormSubmitted": {
13918
+ what: "Form submitted",
13919
+ meaning: "Somebody filled this step in and sent it."
13920
+ },
13921
+ "highlevelEvent.appointmentScheduled": {
13922
+ what: "Appointment booked",
13923
+ meaning: "Somebody picked a slot and confirmed it. This is the booking."
13924
+ },
13925
+ "highlevelEvent.calendarLoaded": {
13926
+ what: "Calendar finished loading",
13927
+ meaning: "The calendar appeared. Nobody has picked a time yet.",
13928
+ lifecycle: true
13929
+ },
13930
+ "highlevelFormEvent.formSubmitted": {
13931
+ what: "Form submitted",
13932
+ meaning: "Somebody filled the form in and sent it."
13933
+ },
13934
+ "highlevelFormEvent.formLoaded": {
13935
+ what: "Form finished loading",
13936
+ meaning: "The form appeared. Nobody has typed anything yet.",
13937
+ lifecycle: true
13938
+ },
13939
+ "hubspotForm.onFormSubmitted": {
13940
+ what: "Form submitted",
13941
+ meaning: "Somebody filled the form in and sent it. This is the one that means a lead."
13942
+ },
13943
+ "hubspotForm.onBeforeFormSubmit": {
13944
+ what: "Form about to be sent",
13945
+ meaning: "Fires an instant before the form is sent \u2014 not the same as it arriving.",
13946
+ lifecycle: true
13947
+ },
13948
+ "hubspotForm.onFormReady": {
13949
+ what: "Form finished loading",
13950
+ meaning: "The form appeared. Nobody has typed anything yet.",
13951
+ lifecycle: true
13952
+ },
13953
+ "hubspotForm.onBeforeFormInit": {
13954
+ what: "Form starting to load",
13955
+ meaning: "Fires before the form has even drawn. It only means the page was opened.",
13956
+ lifecycle: true
13957
+ },
13958
+ "hubspotMeeting.onMeetingScheduled": {
13959
+ what: "Meeting booked",
13960
+ meaning: "Somebody picked a slot and confirmed it. This is the booking."
13961
+ },
13962
+ "link.onClick": {
13963
+ what: "Link clicked",
13964
+ meaning: "Somebody clicked the link on this step and left for wherever it points."
13965
+ },
13966
+ "savvycalEvent.scheduled": {
13967
+ what: "Call booked",
13968
+ meaning: "Somebody picked a slot and confirmed it. This is the booking."
13969
+ },
13970
+ "text.onTextDisplayed": {
13971
+ what: "Screen shown",
13972
+ meaning: "This screen appeared. On a thank-you screen that is the outcome; elsewhere it is not."
13973
+ },
13974
+ "text.onButtonClicked": {
13975
+ what: "Button pressed",
13976
+ meaning: "Somebody pressed the button on this screen."
13977
+ }
13978
+ };
13979
+ function triggerDescription(triggerId) {
13980
+ return TRIGGERS[triggerId];
13654
13981
  }
13655
- function genderLetter(value) {
13656
- const first = value.trim().toLowerCase().charAt(0);
13657
- return first === "f" || first === "m" ? first : null;
13982
+
13983
+ // ../api/src/analytics/feedNoise.ts
13984
+ var BOOKKEEPING_TYPES = /* @__PURE__ */ new Set([
13985
+ "visitor_new",
13986
+ "session_start",
13987
+ "identify",
13988
+ "form_step_view",
13989
+ "side_effect_settled"
13990
+ ]);
13991
+ var SUBMIT_TRIGGERS = new Set(Object.values(STEP_SUBMIT_TRIGGERS));
13992
+ function isEmbedLifecycle(row) {
13993
+ return row.eventType === "form_trigger" && triggerDescription(row.triggerId)?.lifecycle === true;
13658
13994
  }
13659
- function countryCode(value) {
13660
- const code = value.trim().toLowerCase();
13661
- return /^[a-z]{2}$/.test(code) ? code : null;
13995
+ function isBookkeepingRow(row) {
13996
+ if (BOOKKEEPING_TYPES.has(row.eventType)) return true;
13997
+ if (isEmbedLifecycle(row)) return true;
13998
+ return row.eventType === "form_trigger" && SUBMIT_TRIGGERS.has(row.triggerId);
13662
13999
  }
13663
- var MATCH_SPECS = {
13664
- first_name: { keep: "hashed", normalize: tightened, meta: "fn", google: "hashed_first_name" },
13665
- last_name: { keep: "hashed", normalize: tightened, meta: "ln", google: "hashed_last_name" },
13666
- // Meta only. Google's enhanced conversions have no date of birth, so there is
13667
- // no destination wanting it in the clear and every reason not to keep one.
13668
- date_of_birth: { keep: "hashed", normalize: birthDate, meta: "db", google: null },
13669
- gender: { keep: "plain", normalize: genderLetter, meta: "ge", google: null },
13670
- city: { keep: "plain", normalize: loosened, meta: "ct", google: "city" },
13671
- region: { keep: "plain", normalize: loosened, meta: "st", google: "state" },
13672
- postal_code: { keep: "plain", normalize: loosened, meta: "zp", google: "postal_code" },
13673
- country: { keep: "plain", normalize: countryCode, meta: "country", google: "country_code" }
13674
- };
13675
- var MATCH_FIELD_FORWARDING = [
13676
- { field: "email", meta: "em", google: "hashed_email", hashed: true },
13677
- { field: "phone", meta: "ph", google: "hashed_phone_number", hashed: true },
13678
- // Kept in the clear — see `resolveOwnId`. The digest is still computed and
13679
- // still stored beside it, so a Meta forward has one; what changed is that the
13680
- // sender's own key is now readable, which is the entire reason they sent it.
13681
- { field: "external_id", meta: "external_id", google: null, hashed: false },
13682
- ...MATCH_FIELDS.map((field) => ({
13683
- field,
13684
- meta: MATCH_SPECS[field].meta,
13685
- google: MATCH_SPECS[field].google,
13686
- hashed: MATCH_SPECS[field].keep === "hashed"
13687
- })),
13688
- { field: "fbp", meta: "fbp", google: null, hashed: false },
13689
- { field: "fbc", meta: "fbc", google: null, hashed: false }
13690
- ];
13691
14000
 
13692
- // ../api/src/event-ingest/vocabulary.ts
13693
- var INGEST_CLICK_ID_PARAMS = [
14001
+ // ../api/src/analytics/identityLinks.ts
14002
+ var IDENTITY_KEY_KINDS = [
14003
+ "email",
14004
+ "phone",
14005
+ "external",
14006
+ "browser",
13694
14007
  "gclid",
13695
14008
  "wbraid",
13696
14009
  "gbraid",
14010
+ "dclid",
13697
14011
  "fbclid",
13698
14012
  "ttclid",
13699
14013
  "twclid",
13700
14014
  "li_fat_id",
13701
- "msclkid"
14015
+ "msclkid",
14016
+ "rdt_cid",
14017
+ "epik",
14018
+ "sccid",
14019
+ "fbp",
14020
+ "fbc"
13702
14021
  ];
14022
+ var ADDRESS_BOOK_FILL_KINDS = IDENTITY_KEY_KINDS.filter((kind) => kind !== "browser");
13703
14023
 
13704
- // ../api/src/event-sources/fields.ts
13705
- var MATCH_LABELS = {
13706
- first_name: "First name",
13707
- last_name: "Last name",
13708
- date_of_birth: "Date of birth",
13709
- gender: "Gender",
13710
- city: "City",
13711
- region: "Region",
13712
- postal_code: "Postcode",
13713
- country: "Country"
13714
- };
13715
- var CLICK_LABELS = {
13716
- gclid: "Google click id",
13717
- wbraid: "Google click id (iOS, web to app)",
13718
- gbraid: "Google click id (iOS, app)",
13719
- fbclid: "Meta click id",
13720
- ttclid: "TikTok click id",
13721
- twclid: "X click id",
13722
- li_fat_id: "LinkedIn click id",
13723
- msclkid: "Microsoft click id"
14024
+ // ../api/src/analytics/matchKeys.ts
14025
+ var CONTACT_KEYS = ["email", "phone", "external_id", "baker_id"];
14026
+ var MATCH_KEY_FIELDS = [...CONTACT_KEYS, ...INGEST_CLICK_ID_PARAMS, ...MATCH_COOKIES];
14027
+ var MATCH_KEY_LABELS = {
14028
+ email: "email",
14029
+ phone: "phone",
14030
+ external_id: "external ID",
14031
+ baker_id: "Baker ID",
14032
+ gclid: "Google click",
14033
+ wbraid: "Google click (wbraid)",
14034
+ gbraid: "Google click (gbraid)",
14035
+ dclid: "Google Display click",
14036
+ fbclid: "Meta click",
14037
+ ttclid: "TikTok click",
14038
+ twclid: "X click",
14039
+ li_fat_id: "LinkedIn click",
14040
+ msclkid: "Microsoft click",
14041
+ rdt_cid: "Reddit click",
14042
+ epik: "Pinterest click",
14043
+ sccid: "Snapchat click",
14044
+ fbp: "Meta browser cookie",
14045
+ fbc: "Meta click cookie"
13724
14046
  };
13725
- var SOURCE_FIELDS = [
13726
- { field: "event_name", label: "What happened", group: "event" },
13727
- { field: "event_time", label: "When it happened", group: "event" },
13728
- { field: "event_id", label: "Its id", group: "event" },
13729
- { field: "email", label: "Email", group: "person" },
13730
- { field: "phone", label: "Phone", group: "person" },
13731
- { field: "external_id", label: "Your own id for the person", group: "person" },
13732
- ...MATCH_FIELDS.map((field) => ({ field, label: MATCH_LABELS[field], group: "person" })),
13733
- { field: "value", label: "Amount", group: "money" },
13734
- { field: "currency", label: "Currency", group: "money" },
13735
- ...INGEST_CLICK_ID_PARAMS.map((field) => ({ field, label: CLICK_LABELS[field], group: "click" }))
13736
- ];
13737
- var SOURCE_FIELD_NAMES = SOURCE_FIELDS.map((spec) => spec.field);
13738
- var FIELD_SET = new Set(SOURCE_FIELD_NAMES);
13739
- function isSourceField(value) {
13740
- return FIELD_SET.has(value);
13741
- }
13742
- var PROPERTY_ROW_PREFIX = "properties.";
13743
- var PROPERTY_KEY_MAX = 64;
13744
- var PROPERTY_KEY = /^[A-Za-z0-9_][A-Za-z0-9_.-]*$/;
13745
- var RESERVED_PROPERTY_KEYS = ["baker_source", "hubspot_property", "hubspot_value"];
13746
- function isPropertyRow(row) {
13747
- return row.startsWith(PROPERTY_ROW_PREFIX);
13748
- }
13749
- function propertyKeyOf(row) {
13750
- return isPropertyRow(row) ? row.slice(PROPERTY_ROW_PREFIX.length) : null;
13751
- }
13752
- function propertyKeyProblem(key) {
13753
- const trimmed = key.trim();
13754
- if (trimmed === "") return "A property needs a name.";
13755
- if (trimmed.length > PROPERTY_KEY_MAX) return `A property name is at most ${PROPERTY_KEY_MAX} characters.`;
13756
- if (!PROPERTY_KEY.test(trimmed)) {
13757
- return `"${trimmed}" is not a property name: letters, digits, underscore, dash and dot only, starting with a letter or digit.`;
13758
- }
13759
- if (RESERVED_PROPERTY_KEYS.includes(trimmed)) return `"${trimmed}" is written by Baker itself; pick another name.`;
13760
- return null;
13761
- }
13762
- function mappingRowProblem(row) {
13763
- if (isSourceField(row)) return null;
13764
- const key = propertyKeyOf(row);
13765
- if (key === null) {
13766
- return `"${row}" is not a field an event can carry. A field is one of ${SOURCE_FIELD_NAMES.join(", ")}; an extra property is spelled properties.<name>.`;
13767
- }
13768
- return propertyKeyProblem(key);
13769
- }
13770
-
13771
- // ../api/src/event-sources/template.ts
13772
- var MENTION = /\{\{\s*([^{}]+?)\s*\}\}/g;
13773
- function mentionOf(path48) {
13774
- return `{{${path48}}}`;
13775
- }
13776
- function templateMentions(template) {
13777
- const seen = [];
13778
- for (const match of template.matchAll(MENTION)) {
13779
- const path48 = (match[1] ?? "").trim();
13780
- if (path48 !== "" && !seen.includes(path48)) seen.push(path48);
13781
- }
13782
- return seen;
13783
- }
13784
-
13785
- // ../api/src/analytics/sends.ts
13786
- var SEND_PLATFORMS = ["meta", "googleAds", "linkedinInsightTag", "twitterAds", "googleAnalytics"];
13787
- function isSendPlatform(value) {
13788
- return SEND_PLATFORMS.includes(value);
13789
- }
13790
- var SEND_PLATFORM_SPECS = {
13791
- meta: {
13792
- title: "Meta",
13793
- eventMeans: "A Meta event name \u2014 Lead, Purchase, Schedule, or one of your own",
13794
- eventKind: "name",
13795
- credentialFields: ["accessToken", "datasetId"],
13796
- setupHint: "Add the Conversions API access token and dataset id to the Meta pixel in Tracking"
13797
- },
13798
- googleAds: {
13799
- title: "Google Ads",
13800
- eventMeans: "One of the account's conversion actions, picked from its list",
13801
- eventKind: "id",
13802
- credentialFields: ["oauthProviderId"],
13803
- setupHint: "Connect the Google Ads conversion tag to a Google Ads account in Tracking"
13804
- },
13805
- linkedinInsightTag: {
13806
- title: "LinkedIn",
13807
- eventMeans: "The id of a conversion rule in LinkedIn Campaign Manager",
13808
- eventKind: "id",
13809
- credentialFields: ["oauthProviderId"],
13810
- setupHint: "Connect the LinkedIn Insight Tag to a LinkedIn account in Tracking"
13811
- },
13812
- twitterAds: {
13813
- title: "X",
13814
- eventMeans: "The event tag id from X Events Manager, like tw-oka17-abc12",
13815
- eventKind: "id",
13816
- credentialFields: ["oauthProviderId"],
13817
- setupHint: "Connect the X pixel to an X Ads account in Tracking"
13818
- },
13819
- googleAnalytics: {
13820
- title: "Google Analytics",
13821
- eventMeans: "A GA4 event name \u2014 generate_lead, purchase, or one of your own",
13822
- eventKind: "name",
13823
- credentialFields: ["apiSecret"],
13824
- setupHint: "Add the Measurement Protocol API secret to the Google Analytics tag in Tracking"
14047
+ function carries(userData, field) {
14048
+ for (const key of [field, `${field}_sha256`, `${field}_preview`]) {
14049
+ const value = userData[key];
14050
+ if (typeof value === "string" && value !== "") return true;
13825
14051
  }
13826
- };
13827
- var SEND_ORIGINS = ["server", "website", "any"];
13828
- var MAX_SEND_ID = 64;
13829
- var MAX_SEND_EVENT_KEY = 320;
13830
- function sendEventIsTemplated(event) {
13831
- return templateMentions(event).length > 0;
14052
+ return false;
13832
14053
  }
13833
- function sendEventProblem(platform, event) {
13834
- if (!isSendPlatform(platform)) return null;
13835
- if (!sendEventIsTemplated(event)) return null;
13836
- const spec = SEND_PLATFORM_SPECS[platform];
13837
- if (spec.eventKind === "name") return null;
13838
- return `${spec.title} identifies a conversion by an id it issued, so it cannot be written from the event. Paste the id; pass what varies as an extra parameter instead.`;
14054
+ function rowMatchKeys(userData) {
14055
+ if (userData === void 0) return [];
14056
+ return MATCH_KEY_FIELDS.filter((field) => carries(userData, field)).map((field) => ({
14057
+ field,
14058
+ label: MATCH_KEY_LABELS[field] ?? field
14059
+ }));
13839
14060
  }
13840
14061
 
13841
14062
  // ../api/src/analytics/sendPayload.ts
@@ -13858,11 +14079,15 @@ var CLICK_LABELS2 = {
13858
14079
  gclid: "Google click id",
13859
14080
  wbraid: "Google click id (web-to-app)",
13860
14081
  gbraid: "Google click id (app-to-web)",
14082
+ dclid: "Google click id (Display & Video)",
13861
14083
  fbclid: "Meta click id",
13862
14084
  ttclid: "TikTok click id",
13863
14085
  twclid: "X click id",
13864
14086
  li_fat_id: "LinkedIn click id",
13865
- msclkid: "Microsoft click id"
14087
+ msclkid: "Microsoft click id",
14088
+ rdt_cid: "Reddit click id",
14089
+ epik: "Pinterest click id",
14090
+ sccid: "Snapchat click id"
13866
14091
  };
13867
14092
  var PERSON_FIELDS = MATCH_FIELD_FORWARDING.map((row) => ({
13868
14093
  name: row.field,
@@ -14572,11 +14797,25 @@ var analyticsQueryRequestSchema = z31.object({
14572
14797
  /**
14573
14798
  * One browser, by its cookie id.
14574
14799
  *
14575
- * Narrows `people` to a single person, and replays that person's whole
14576
- * history on `session` — every visit they made, not just the one. Given
14577
- * with `sessionId`, the session wins and this is ignored.
14800
+ * Narrows `people` to a single person, narrows `stream` to everything
14801
+ * that person did, and replays that person's whole history on `session` —
14802
+ * every visit they made, not just the one. Given with `sessionId`, the
14803
+ * session wins and this is ignored.
14804
+ *
14805
+ * On `stream` it is a seed rather than a cookie filter: the browser plus
14806
+ * every address, phone, click id and platform cookie ever seen beside it.
14807
+ * Filtering on the cookie alone would drop exactly the rows the reader
14808
+ * came for — a deal a CRM posted names the person and carries no cookie.
14578
14809
  */
14579
14810
  anonId: z31.string().max(120).optional(),
14811
+ /**
14812
+ * `stream` and `session`: one person by any key the store recognises them
14813
+ * by — a contact digest, a CRM id, a click id, a platform cookie — for the
14814
+ * rows a system posted about somebody no browser has named yet. A browser
14815
+ * wins when both are given.
14816
+ */
14817
+ personKind: z31.string().max(40).optional(),
14818
+ personValue: z31.string().max(255).optional(),
14580
14819
  /**
14581
14820
  * `people` only: keep only the visitors who converted.
14582
14821
  *
@@ -14613,9 +14852,9 @@ var analyticsQueryRequestSchema = z31.object({
14613
14852
  message: "The 'page' preset needs --path",
14614
14853
  path: ["path"]
14615
14854
  }).refine(
14616
- (value) => value.preset !== "session" || typeof value.sessionId === "string" || typeof value.anonId === "string",
14855
+ (value) => value.preset !== "session" || typeof value.sessionId === "string" || typeof value.anonId === "string" || typeof value.personValue === "string",
14617
14856
  {
14618
- message: "The 'session' preset needs --session or a browser id",
14857
+ message: "The 'session' preset needs --session, a browser id or a person key",
14619
14858
  path: ["sessionId"]
14620
14859
  }
14621
14860
  ).refine((value) => value.property === void 0 || typeof value.eventName === "string", {
@@ -14646,6 +14885,13 @@ var analyticsTotalsSchema = z31.object({
14646
14885
  * {@link convertingSessions}, and of `sessions`.
14647
14886
  */
14648
14887
  conversions: z31.number().int().nonnegative(),
14888
+ /**
14889
+ * Conversions in the window that no landing earned — by people none of the
14890
+ * company's pages saw. Not in `conversions`, which is a page metric; shown
14891
+ * beside it so a deal a CRM reported is never a silent zero. Zero under any
14892
+ * scope.
14893
+ */
14894
+ unplacedConversions: z31.number().int().nonnegative().default(0),
14649
14895
  /** Visits that produced at least one conversion. Never above `sessions`. */
14650
14896
  convertingSessions: z31.number().int().nonnegative(),
14651
14897
  /**
@@ -15138,8 +15384,14 @@ var analyticsConversionRowSchema = z31.object({
15138
15384
  * accident; saying so is what makes it findable.
15139
15385
  */
15140
15386
  countMode: z31.string(),
15141
- /** Counted the way each definition asked to be counted. */
15387
+ /** Counted the way each definition asked to be counted, and credited to a landing. */
15142
15388
  conversions: z31.number(),
15389
+ /**
15390
+ * Times it happened that no landing earned: by people none of the company's
15391
+ * pages saw, within 90 days. Zero under a landing or an ad scope, where the
15392
+ * gap would mean something else. Absent on a row built before it existed.
15393
+ */
15394
+ unplaced: z31.number().default(0),
15143
15395
  /** Distinct visits that produced it, never mode-adjusted. The gap between the two is the repeat behaviour. */
15144
15396
  convertingVisits: z31.number(),
15145
15397
  /** Null when it has never fired. */
@@ -15633,7 +15885,8 @@ var analyticsVisitorRowSchema = z31.object({
15633
15885
  campaignParams: z31.record(z31.string(), z31.string()),
15634
15886
  /**
15635
15887
  * The click ids this person arrived on, by their own names — `gclid`,
15636
- * `fbclid`, `msclkid`, `li_fat_id`, `ttclid`, `twclid`, `wbraid`, `gbraid`.
15888
+ * `fbclid`, `msclkid`, `li_fat_id`, `ttclid`, `twclid`, `wbraid`, `gbraid`,
15889
+ * `dclid`, `rdt_cid`, `epik`, `sccid`.
15637
15890
  *
15638
15891
  * Only the ones that were present. The endpoint has always returned all eight
15639
15892
  * columns and nothing above it carried them, so the one screen whose subject
@@ -29229,9 +29482,15 @@ var EVENT_INGEST_BODY_MAX_BYTES = 8 * 1024 * 1024;
29229
29482
  var EVENT_INGEST_REQUESTS_PER_MINUTE = 60;
29230
29483
  var FUTURE_TOLERANCE_MS = 5 * 6e4;
29231
29484
  var MAX_EVENT_NAME = 80;
29485
+ var EVENT_NAME = /^[\p{L}\p{N}][\p{L}\p{M}\p{N} ._-]*$/u;
29486
+ function validateEventName(name) {
29487
+ const value = typeof name === "string" ? name.trim() : "";
29488
+ if (value === "" || value.length > MAX_EVENT_NAME || !EVENT_NAME.test(value)) return "invalid_name";
29489
+ return null;
29490
+ }
29232
29491
  var REJECTION_MESSAGES = {
29233
29492
  invalid_event_id: "event_id, when sent, must be 1-128 characters of A-Z a-z 0-9 . _ : -",
29234
- invalid_name: "event_name is required: up to 80 characters of letters, digits, spaces, . _ and -",
29493
+ invalid_name: "event_name is required: up to 80 characters of letters (any language), digits, spaces, . _ and -, starting with a letter or a digit",
29235
29494
  missing_identity: "user_data named nobody, and no url named a page",
29236
29495
  invalid_timestamp: "event_time must be an ISO-8601 instant with an offset or Unix seconds, and not in the future",
29237
29496
  invalid_value: "value and currency go together: a non-negative decimal and an ISO-4217 code",
@@ -29259,8 +29518,8 @@ var EVENT_FIELDS = [
29259
29518
  {
29260
29519
  name: "event_name",
29261
29520
  need: "required",
29262
- takes: `What happened. Up to ${MAX_EVENT_NAME} characters of letters, digits, spaces, . _ and -`,
29263
- note: "The only required field. Use one of the standard names below wherever one fits \u2014 anything else is yours to invent."
29521
+ takes: `What happened. Up to ${MAX_EVENT_NAME} characters of letters, digits, spaces, . _ and -, starting with a letter or a digit`,
29522
+ note: 'The only required field. Use one of the standard names below wherever one fits \u2014 anything else is yours to invent, in whatever language you name things in: a letter is a letter in any script, and "Matr\xEDcula Realizada" is a name.'
29264
29523
  },
29265
29524
  {
29266
29525
  name: "event_time",
@@ -29311,7 +29570,7 @@ var EVENT_FIELDS = [
29311
29570
  name: "the ad click ids",
29312
29571
  need: "optional",
29313
29572
  under: true,
29314
- takes: "The click id that brought them, if you kept it, under the name the platform stamps on the link: gclid, wbraid, gbraid, fbclid, ttclid, twclid, li_fat_id, msclkid.",
29573
+ takes: "The click id that brought them, if you kept it, under the name the platform stamps on the link: gclid, wbraid, gbraid, dclid, fbclid, ttclid, twclid, li_fat_id, msclkid, rdt_cid, epik, sccid (Snapchat's ScCid, in any case).",
29315
29574
  note: "A click id names a person too \u2014 it is enough on its own, and it attributes the event to the campaign that earned it."
29316
29575
  },
29317
29576
  {
@@ -30114,6 +30373,7 @@ function mergeStoredWithCounts(stored, reported) {
30114
30373
  // would make it indistinguishable from one nobody has named.
30115
30374
  conversions: counted?.conversions ?? 0,
30116
30375
  convertingVisits: counted?.convertingVisits ?? 0,
30376
+ unplaced: counted?.unplaced ?? 0,
30117
30377
  lastSeen: counted?.lastSeen ?? null,
30118
30378
  doors: counted?.doors ?? [],
30119
30379
  ...rows.some((row) => row.staged === true) ? { staged: true } : {}
@@ -30121,6 +30381,40 @@ function mergeStoredWithCounts(stored, reported) {
30121
30381
  }).sort((a, b) => a.name.localeCompare(b.name));
30122
30382
  }
30123
30383
 
30384
+ // src/commands/analytics/feedRows.ts
30385
+ function isStreamRow(value) {
30386
+ return typeof value === "object" && value !== null && !Array.isArray(value);
30387
+ }
30388
+ function enrich(row) {
30389
+ const out = { ...row };
30390
+ const destination = typeof row.sideEffectType === "string" ? row.sideEffectType : "";
30391
+ if (destination !== "") out.destination = destinationName(destination);
30392
+ const keys = rowMatchKeys(row.userData);
30393
+ if (keys.length > 0) out.matchedBy = keys.map((key) => key.field);
30394
+ return out;
30395
+ }
30396
+ function shapeFeed(stream, everything) {
30397
+ if (!Array.isArray(stream)) return { rows: [], heldBack: 0 };
30398
+ const rows = stream.filter(isStreamRow);
30399
+ const kept = everything ? rows : rows.filter((row) => !isBookkeepingRow({ eventType: row.eventType ?? "", triggerId: row.triggerId ?? "" }));
30400
+ return { rows: kept.map(enrich), heldBack: rows.length - kept.length };
30401
+ }
30402
+ function feedHints(shape) {
30403
+ const hints2 = [];
30404
+ if (shape.heldBack > 0) {
30405
+ hints2.push(
30406
+ `${shape.heldBack} ${shape.heldBack === 1 ? "row is" : "rows are"} not listed: rows recorded to work the reports out \u2014 identifications, step views, session starts, a destination's answer already folded onto its dispatch \u2014 and embedded forms and calendars announcing that they drew. Add --everything to see them.`
30407
+ );
30408
+ }
30409
+ const upload = shape.rows.find((row) => row.sideEffectType !== void 0 && row.eventId !== void 0);
30410
+ if (upload !== void 0) {
30411
+ hints2.push(
30412
+ `A delivery row carries the event it was made for in \`eventId\`. \`baker analytics feed --uid ${String(upload.eventId)}\` returns that event and every upload made for it, each with the platform's verdict.`
30413
+ );
30414
+ }
30415
+ return hints2;
30416
+ }
30417
+
30124
30418
  // src/commands/analytics/flowConversionKeys.ts
30125
30419
  function formKeyRefusal(eventKey, tree, slug) {
30126
30420
  const parsed = parseEventKey(eventKey);
@@ -30752,10 +31046,10 @@ function sendsWrite(args4) {
30752
31046
  return parsed.data;
30753
31047
  }
30754
31048
  function sendsView(response, full) {
30755
- const { carries, templateFields, ...rest } = response.data;
31049
+ const { carries: carries2, templateFields, ...rest } = response.data;
30756
31050
  return {
30757
31051
  ok: true,
30758
- data: { ...rest, ...full ? { carries, templateFields } : {} },
31052
+ data: { ...rest, ...full ? { carries: carries2, templateFields } : {} },
30759
31053
  ...response.hints && response.hints.length > 0 ? { hints: response.hints } : {}
30760
31054
  };
30761
31055
  }
@@ -31009,6 +31303,7 @@ import { z as z36 } from "zod";
31009
31303
  var SOURCE_KINDS = ["webhook", "hubspot"];
31010
31304
  var SOURCE_STATUSES = ["learning", "counting", "paused"];
31011
31305
  var SOURCE_NAME_MAX = 80;
31306
+ var sourceNameSchema = z36.string().trim().min(1).max(SOURCE_NAME_MAX).refine((name) => validateEventName(name) === null, REJECTION_MESSAGES.invalid_name);
31012
31307
  var sourceIdSchema = z36.string().min(1);
31013
31308
  var sourceMappingSchema = z36.record(z36.string(), z36.string().min(1)).superRefine((mapping, ctx) => {
31014
31309
  for (const row of Object.keys(mapping)) {
@@ -31084,10 +31379,10 @@ var hubspotPreviewSchema = z36.strictObject({
31084
31379
  var sourcesRequestSchema = z36.strictObject({
31085
31380
  /** The company's own server door: made once, `counting` from the start, the same address for ever after. */
31086
31381
  server: z36.strictObject({}).optional(),
31087
- create: z36.strictObject({ name: z36.string().trim().min(1).max(SOURCE_NAME_MAX) }).optional(),
31382
+ create: z36.strictObject({ name: sourceNameSchema }).optional(),
31088
31383
  /** A HubSpot event: read every five minutes, counting from now. The labels are looked up server-side. */
31089
31384
  createHubspot: z36.strictObject({
31090
- name: z36.string().trim().min(1).max(SOURCE_NAME_MAX).optional(),
31385
+ name: sourceNameSchema.optional(),
31091
31386
  /** A contact, or a deal. A deal's event is about its contact, read through `contact.<property>` rows. */
31092
31387
  object: hubspotObjectSchema.default("contact"),
31093
31388
  trigger: hubspotTriggerInputSchema,
@@ -31113,11 +31408,46 @@ var sourcesRequestSchema = z36.strictObject({
31113
31408
  amountProperty: z36.string().trim().min(1).optional(),
31114
31409
  currency: currencyCodeSchema.optional()
31115
31410
  }).optional(),
31411
+ /**
31412
+ * Count a HubSpot event's history: every moment its trigger came to hold
31413
+ * since `since`, read out of HubSpot's own property history.
31414
+ *
31415
+ * Not the thing a source deliberately refuses to do. A source never
31416
+ * backfills the **present state** — somebody who already satisfies a
31417
+ * condition is not an event today — and this reports only moments HubSpot
31418
+ * has a timestamp for, so nothing is invented. Dry unless `write`.
31419
+ */
31420
+ backfill: z36.strictObject({
31421
+ sourceId: sourceIdSchema,
31422
+ /** ISO instant, or Unix milliseconds. Bounded by the 400 days the store keeps. */
31423
+ since: z36.union([z36.string().trim().min(1), z36.number()]),
31424
+ /**
31425
+ * Where a capped run carries on from: only records last changed at or
31426
+ * before this instant are read.
31427
+ *
31428
+ * The **read** is what it bounds, never the history. Records are chosen
31429
+ * most-recently-changed first, so a run that reaches its cap has covered
31430
+ * the newest of them and the rest are the ones nearest `since` — which
31431
+ * no shorter window can reach, because shortening one only drops
31432
+ * candidates from the old end and hands back the same newest records
31433
+ * again. `reachedBack` is the value to pass here, and the moments each
31434
+ * record is then judged on are still the whole of `since` to now.
31435
+ */
31436
+ before: z36.union([z36.string().trim().min(1), z36.number()]).optional(),
31437
+ /** Without it, nothing is counted and the answer says what would be. */
31438
+ write: z36.boolean().optional()
31439
+ }).optional(),
31116
31440
  map: z36.strictObject({ sourceId: sourceIdSchema, mapping: sourceMappingSchema }).optional(),
31117
31441
  pause: z36.strictObject({ sourceId: sourceIdSchema }).optional(),
31118
31442
  resume: z36.strictObject({ sourceId: sourceIdSchema }).optional(),
31119
31443
  remove: z36.strictObject({ sourceId: sourceIdSchema }).optional()
31120
31444
  });
31445
+ var sourceRefusalSchema = z36.strictObject({
31446
+ at: z36.number(),
31447
+ count: z36.number(),
31448
+ code: z36.string(),
31449
+ reason: z36.string()
31450
+ });
31121
31451
  var detectedFieldSchema = z36.strictObject({
31122
31452
  path: z36.string(),
31123
31453
  confidence: z36.enum(["key", "value"])
@@ -31145,6 +31475,14 @@ var sourceViewSchema = z36.strictObject({
31145
31475
  /** Why the last delivery could not be counted, in product words; absent when the last one was. */
31146
31476
  lastError: z36.string().optional(),
31147
31477
  lastArrivalAt: z36.number().optional(),
31478
+ /**
31479
+ * What its last delivery **refused, event by event** — which `lastError` is
31480
+ * not and `lastArrivalAt` cannot be. A source refusing everything it
31481
+ * receives has no arrival and no read failure, so without this it reads as
31482
+ * one nobody has sent anything to. `refusalWords` turns it into the
31483
+ * sentence a person is shown. Cleared by the next delivery that counts.
31484
+ */
31485
+ lastRefusal: sourceRefusalSchema.optional(),
31148
31486
  /** The first send, masked, while learning. Kept once confirmed. */
31149
31487
  sample: z36.strictObject({ receivedAt: z36.number(), body: z36.string() }).optional(),
31150
31488
  /** Field → template; empty means the guess (an address) or the defaults (a HubSpot read). */
@@ -31166,12 +31504,37 @@ var stagedSourceOpSchema = z36.strictObject({
31166
31504
  mapping: z36.record(z36.string(), z36.string()).optional(),
31167
31505
  status: z36.enum(["paused", "counting"]).optional()
31168
31506
  });
31507
+ var backfillResultSchema = z36.strictObject({
31508
+ sourceId: z36.string(),
31509
+ name: z36.string(),
31510
+ since: z36.number(),
31511
+ /** Records whose history was read. */
31512
+ examined: z36.number(),
31513
+ /** Moments the trigger came to hold, across them. */
31514
+ entries: z36.number(),
31515
+ /** Of those, already counted — the journal got there first, or a previous run did. */
31516
+ alreadyCounted: z36.number(),
31517
+ /** What a `write` run would hand over, or did. */
31518
+ pending: z36.number(),
31519
+ /** Whether the read stopped at its cap; {@link BackfillResult.reachedBack} is where to carry on from. */
31520
+ capped: z36.boolean(),
31521
+ /**
31522
+ * How far back the read got: the instant the oldest record it examined was
31523
+ * last changed. Only on a capped run, and it is the next run's `before`.
31524
+ */
31525
+ reachedBack: z36.number().optional(),
31526
+ counted: z36.number().optional(),
31527
+ refused: z36.number().optional(),
31528
+ refusedReason: z36.string().optional()
31529
+ });
31169
31530
  var sourcesResponseSchema = z36.strictObject({
31170
31531
  ok: z36.literal(true),
31171
31532
  data: z36.strictObject({
31172
31533
  sources: z36.array(sourceViewSchema),
31173
31534
  /** Only when the request carried `preview`. */
31174
31535
  preview: hubspotPreviewSchema.optional(),
31536
+ /** Only when the request carried `backfill`. */
31537
+ backfill: backfillResultSchema.optional(),
31175
31538
  /** What this chat has staged, when one is attached: applied at Publish, dropped with a discard. */
31176
31539
  staged: z36.array(stagedSourceOpSchema).optional()
31177
31540
  }),
@@ -31282,6 +31645,26 @@ var args2 = {
31282
31645
  description: "Change where a HubSpot-read source takes its amount from. Needs --source; with --amount-property and --currency it reads them, with neither it stops reading an amount",
31283
31646
  required: false
31284
31647
  },
31648
+ backfill: {
31649
+ type: "boolean",
31650
+ description: "Count a HubSpot event's history: every moment its trigger came to hold since --since, at the instant HubSpot says it did. Needs --source and --since. Nothing is invented \u2014 a record that merely satisfies the trigger today is not an event today, and only moments HubSpot has a timestamp for are counted. Dry without --write. Reach for it when an event was made after the deals it should have counted, or after fixing one that was refusing everything it received",
31651
+ required: false
31652
+ },
31653
+ since: {
31654
+ type: "string",
31655
+ description: "With --backfill: the date to count this event's history from, `2026-09-19` or an ISO instant. At most 400 days back \u2014 the store keeps no more",
31656
+ required: false
31657
+ },
31658
+ before: {
31659
+ type: "string",
31660
+ description: "With --backfill: carry on from where a capped run stopped, by passing back the `reachedBack` it returned. One run reads at most 500 records, most recently changed first, so a capped run has covered the newest and the rest are the ones nearest --since \u2014 narrowing --since cannot reach them, this can. Repeat until `capped` is false; nothing is counted twice. It bounds which records are read, never which of their moments count: those are still the whole of --since to now",
31661
+ required: false
31662
+ },
31663
+ write: {
31664
+ type: "boolean",
31665
+ description: "With --backfill: actually count what it found. Without it nothing is counted and the answer says what would be. Events inside the last 72 hours are uploaded to this event's ad platforms within minutes of counting, which spends real budget",
31666
+ required: false
31667
+ },
31285
31668
  name: {
31286
31669
  type: "string",
31287
31670
  description: "The tool the source is for \u2014 HubSpot, Zapier, Stripe. It is the event's name when the payload does not say one, and every event it sends carries it as `baker_source`",
@@ -31445,14 +31828,37 @@ var VERBS = [
31445
31828
  "create-hubspot",
31446
31829
  "set-trigger",
31447
31830
  "preview",
31831
+ "backfill",
31448
31832
  "set-amount",
31449
31833
  "map",
31450
31834
  "pause",
31451
31835
  "resume",
31452
31836
  "remove"
31453
31837
  ];
31454
- function sourcesRequest(raw, argv) {
31838
+ function checkedName(raw, argv) {
31455
31839
  const name = onlyOnce(raw.name, "--name", argv);
31840
+ if (name === void 0 || validateEventName(name) === null) return name;
31841
+ throw new ConversionArgsError(
31842
+ `"${name}" cannot be an event name, and the name is what every event this source raises will carry. ${REJECTION_MESSAGES.invalid_name}`
31843
+ );
31844
+ }
31845
+ function backfillArgs(raw, argv, sourceId) {
31846
+ const since = onlyOnce(raw.since, "--since", argv);
31847
+ if (since === void 0) {
31848
+ throw new ConversionArgsError(
31849
+ "--backfill needs --since <date>, e.g. --since 2026-09-19. Nothing before it is read."
31850
+ );
31851
+ }
31852
+ const before = onlyOnce(raw.before, "--before", argv);
31853
+ return {
31854
+ sourceId,
31855
+ since,
31856
+ ...before === void 0 ? {} : { before },
31857
+ ...raw.write === true ? { write: true } : {}
31858
+ };
31859
+ }
31860
+ function sourcesRequest(raw, argv) {
31861
+ const name = checkedName(raw, argv);
31456
31862
  const source = onlyOnce(raw.source, "--source", argv);
31457
31863
  const verbs = VERBS.filter((verb2) => raw[verb2] === true);
31458
31864
  if (verbs.length > 1) throw new ConversionArgsError(`One of --${verbs.join(", --")} per call.`);
@@ -31466,6 +31872,7 @@ function sourcesRequest(raw, argv) {
31466
31872
  if (verb === "create-hubspot") return { createHubspot: createHubspotArgs(raw, argv, name) };
31467
31873
  if (verb === "preview") return { preview: previewArgs(raw, argv) };
31468
31874
  if (source === void 0) throw new ConversionArgsError(`--${verb} needs --source <id>, from the list.`);
31875
+ if (verb === "backfill") return { backfill: backfillArgs(raw, argv, source) };
31469
31876
  if (verb === "set-trigger") {
31470
31877
  const contacts = contactsArg(raw, argv);
31471
31878
  return {
@@ -31754,7 +32161,10 @@ async function runPreset(args4, options) {
31754
32161
  process.exit(1);
31755
32162
  }
31756
32163
  const data = pruneForAgent(response.data, args4.full === true);
32164
+ const feed = "stream" in data ? shapeFeed(data.stream, args4.everything === true) : void 0;
32165
+ if (feed !== void 0) data.stream = feed.rows;
31757
32166
  const hints2 = [
32167
+ ...feed === void 0 ? [] : feedHints(feed),
31758
32168
  // The platform the ad blocks were scoped to, so the tagging hint can
31759
32169
  // only speak about the inventory it was actually given. `adParams` is one
31760
32170
  // platform's list; a hint drawn from it without knowing which platform
@@ -31984,7 +32394,8 @@ function feedNarrowing(args4) {
31984
32394
  ...args4.kind ? { eventKey: String(args4.kind) } : {},
31985
32395
  ...args4.send ? { sendId: String(args4.send) } : {},
31986
32396
  ...args4["sent-to"] ? { sentTo: String(args4["sent-to"]) } : {},
31987
- ...args4.uid ? { eventUid: String(args4.uid) } : {}
32397
+ ...args4.uid ? { eventUid: String(args4.uid) } : {},
32398
+ ...args4.person ? { anonId: String(args4.person) } : {}
31988
32399
  };
31989
32400
  }
31990
32401
  var feedCommand = presetCommand({
@@ -32026,6 +32437,16 @@ var feedCommand = presetCommand({
32026
32437
  type: "string",
32027
32438
  description: "One kind of event, by the key `baker analytics conversions` lists (page:Deal won, form:contact:\u2026)",
32028
32439
  required: false
32440
+ },
32441
+ person: {
32442
+ type: "string",
32443
+ description: "One person and everything they did, seeded by a browser id from the `anonId` on any row. A seed rather than a cookie filter: the store resolves it to the whole person \u2014 across devices, and across every address, phone, customer id, ad click and platform cookie ever seen beside that browser \u2014 so a deal their CRM posted, which carries no cookie at all, is on the list beside the page view that named them",
32444
+ required: false
32445
+ },
32446
+ everything: {
32447
+ type: "boolean",
32448
+ description: "Include the rows held back by default: the ones recorded so the reports can be worked out (identifications, step views, session starts, a destination's answer already folded onto its dispatch) and embedded forms and calendars announcing that they drew. Reach for it when debugging the collector rather than an integration \u2014 an embed that draws and never fires again is only visible here",
32449
+ required: false
32029
32450
  }
32030
32451
  },
32031
32452
  resolve: (args4) => feedRequest(args4)