@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/README.md +13 -1
- package/dist/cli.js +636 -215
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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/
|
|
13628
|
-
var
|
|
13629
|
-
"
|
|
13630
|
-
|
|
13631
|
-
|
|
13632
|
-
|
|
13633
|
-
"
|
|
13634
|
-
|
|
13635
|
-
|
|
13636
|
-
|
|
13637
|
-
|
|
13638
|
-
|
|
13639
|
-
|
|
13640
|
-
|
|
13641
|
-
|
|
13642
|
-
|
|
13643
|
-
|
|
13644
|
-
|
|
13645
|
-
|
|
13646
|
-
}
|
|
13647
|
-
|
|
13648
|
-
|
|
13649
|
-
|
|
13650
|
-
|
|
13651
|
-
|
|
13652
|
-
|
|
13653
|
-
|
|
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
|
-
|
|
13656
|
-
|
|
13657
|
-
|
|
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
|
|
13660
|
-
|
|
13661
|
-
|
|
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/
|
|
13693
|
-
var
|
|
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/
|
|
13705
|
-
var
|
|
13706
|
-
|
|
13707
|
-
|
|
13708
|
-
|
|
13709
|
-
|
|
13710
|
-
|
|
13711
|
-
|
|
13712
|
-
|
|
13713
|
-
|
|
13714
|
-
|
|
13715
|
-
|
|
13716
|
-
|
|
13717
|
-
|
|
13718
|
-
|
|
13719
|
-
|
|
13720
|
-
|
|
13721
|
-
|
|
13722
|
-
|
|
13723
|
-
|
|
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
|
-
|
|
13726
|
-
|
|
13727
|
-
|
|
13728
|
-
|
|
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
|
|
13834
|
-
if (
|
|
13835
|
-
|
|
13836
|
-
|
|
13837
|
-
|
|
13838
|
-
|
|
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,
|
|
14576
|
-
*
|
|
14577
|
-
*
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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)
|