@koda-sl/baker-cli 0.307.0-dev.25b4836d3 → 0.307.0-dev.48246ea89

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
@@ -4816,6 +4816,8 @@ var chatChangeTypeSchema = z13.enum([
4816
4816
  "conversion-sends",
4817
4817
  // The company's sources: a HubSpot event made or changed. Staged, applied at Publish.
4818
4818
  "event-sources",
4819
+ // What the company counts as a conversion, when an agent changed it. Staged, applied at Publish.
4820
+ "conversions",
4819
4821
  // An A/B test between a landing and its alternative. Staged, not immediate.
4820
4822
  "experiment",
4821
4823
  "briefs"
@@ -14950,7 +14952,9 @@ var analyticsConversionRowSchema = z30.object({
14950
14952
  * The doors its events came in by in the window, in
14951
14953
  * {@link ANALYTICS_EVENT_DOORS} order. Empty when it has not fired.
14952
14954
  */
14953
- doors: z30.array(z30.enum(ANALYTICS_EVENT_DOORS)).default([])
14955
+ doors: z30.array(z30.enum(ANALYTICS_EVENT_DOORS)).default([]),
14956
+ /** Proposed in this chat: counted from when it is published, so every report reads it as nothing until then. */
14957
+ staged: z30.boolean().optional()
14954
14958
  });
14955
14959
  var analyticsEventCatalogRowSchema = z30.object({
14956
14960
  /** `form:…` | `submit:…` | `page:…` | `exit:…` — parse with `parseEventKey`. */
@@ -15558,14 +15562,24 @@ var analyticsConversionsRequestSchema = z30.object({
15558
15562
  var analyticsConversionsResponseSchema = z30.object({
15559
15563
  ok: z30.literal(true),
15560
15564
  data: z30.object({
15565
+ /**
15566
+ * What the company counts — inside a chat, with this chat's proposal on
15567
+ * top: `staged` marks a definition that applies when the chat is published.
15568
+ */
15561
15569
  definitions: z30.array(
15562
15570
  z30.object({
15563
15571
  eventKey: z30.string(),
15564
15572
  name: z30.string(),
15565
15573
  countMode: z30.string(),
15566
- updatedAt: z30.number()
15574
+ updatedAt: z30.number(),
15575
+ staged: z30.boolean().optional()
15567
15576
  })
15568
- )
15577
+ ),
15578
+ /** This chat's proposal, when it has one: counted from Publish, and gone if the chat is discarded. */
15579
+ staged: z30.object({
15580
+ set: z30.array(z30.object({ eventKey: z30.string(), name: z30.string() })),
15581
+ remove: z30.array(z30.string())
15582
+ }).optional()
15569
15583
  }),
15570
15584
  hints: z30.array(z30.string()).optional()
15571
15585
  });
@@ -29278,6 +29292,14 @@ function namingHints(definitions) {
29278
29292
  `Names in use here: ${names.join(", ")}. Reuse one EXACTLY when another Form produces the same outcome \u2014 that is what makes them one number \u2014 and use \`--rename "<old>" --to "<existing>"\` to merge two that should always have been one.`
29279
29293
  ];
29280
29294
  }
29295
+ function stagedHints(definitions) {
29296
+ const staged = definitions.filter((row) => row.staged === true);
29297
+ if (staged.length === 0) return [];
29298
+ return [
29299
+ `Staged on this chat, not counted yet: ${staged.map((row) => `"${row.name}"`).join(", ")}. The person sees ${staged.length === 1 ? "it" : "them"} under Conversions in the chat's changes, and ${staged.length === 1 ? "it counts" : "they count"} \u2014 history included \u2014 from when they publish the chat; discarding the chat drops ${staged.length === 1 ? "it" : "them"}. Tell them that, not that it is already counted.`,
29300
+ 'When you talk to the person, call each event by its name ("Appointment scheduled"), never its key (`page:Appointment scheduled`): the key is how Baker spells it internally, and the `page:` part means nothing to them.'
29301
+ ];
29302
+ }
29281
29303
 
29282
29304
  // src/commands/analytics/conversionList.ts
29283
29305
  function mergeStoredWithCounts(stored, reported) {
@@ -29303,7 +29325,8 @@ function mergeStoredWithCounts(stored, reported) {
29303
29325
  conversions: counted?.conversions ?? 0,
29304
29326
  convertingVisits: counted?.convertingVisits ?? 0,
29305
29327
  lastSeen: counted?.lastSeen ?? null,
29306
- doors: counted?.doors ?? []
29328
+ doors: counted?.doors ?? [],
29329
+ ...rows.some((row) => row.staged === true) ? { staged: true } : {}
29307
29330
  };
29308
29331
  }).sort((a, b) => a.name.localeCompare(b.name));
29309
29332
  }
@@ -31430,6 +31453,7 @@ function conversionsEnvelope(args4, definitions, candidates) {
31430
31453
  const hints2 = [
31431
31454
  ...flow ? flowConversionHints(flow.slug, flow.endings, flow.noConversionReason) : [],
31432
31455
  ...flowCoverageHints(coverage),
31456
+ ...stagedHints(definitions),
31433
31457
  ...page.hints,
31434
31458
  ...namingHints(definitions)
31435
31459
  ];
@@ -31448,7 +31472,7 @@ var conversionsCommand3 = (() => {
31448
31472
  const args4 = {
31449
31473
  event: {
31450
31474
  type: "string",
31451
- description: "The event to start counting, from the `candidates` or `--flow` list: form:<form>:<step>:<trigger>, submit:<form>, page:<name>, or exit:<host>. Requires --name. REPEATABLE \u2014 pass it several times with one --name to count all of them as that single outcome, which is the whole point of the name. Applies immediately and scores the WHOLE history, not just from now on, so counting the right event today also corrects last month's reports. An event a client's system sends (the API, HubSpot) counts here too: it is credited to the LAST LANDING its person viewed, up to 90 days before, so a deal won weeks later is that landing's conversion. A person who never saw a landing is not counted at all. Such an event is usually also a funnel step (`baker funnel`) and a send (`baker analytics sends`)",
31475
+ description: "The event to start counting, from the `candidates` or `--flow` list: form:<form>:<step>:<trigger>, submit:<form>, page:<name>, or exit:<host>. Requires --name. REPEATABLE \u2014 pass it several times with one --name to count all of them as that single outcome, which is the whole point of the name. Inside a chat it is STAGED: the chat lists it under Conversions and it counts from when the chat is published \u2014 then it scores the WHOLE history, not just from then on, so counting the right event also corrects last month's reports. An event a client's system sends (the API, HubSpot) counts here too: it is credited to the LAST LANDING its person viewed, up to 90 days before, so a deal won weeks later is that landing's conversion. A person who never saw a landing is not counted at all. Such an event is usually also a funnel step (`baker funnel`) and a send (`baker analytics sends`)",
31452
31476
  required: false
31453
31477
  },
31454
31478
  name: {
@@ -31505,7 +31529,7 @@ var conversionsCommand3 = (() => {
31505
31529
  return defineCommand97({
31506
31530
  meta: {
31507
31531
  name: "conversions",
31508
- description: "What this company counts as a conversion ON ITS PAGES \u2014 a browser event, once per visit: the numerator of every conversion rate on Measure, and nothing more. An event one of the client's systems sends (a deal won in HubSpot, an order the server posts) is not a page conversion: it is a step on the funnel (`baker funnel`) and can be sent to an ad platform (`baker analytics sends`). Run with no flags first: an empty `definitions` list means every conversion number in every other report is zero \u2014 not because nobody converted, but because nothing is marked as converting, and Baker never guesses. Add `--candidates` to see everything these pages actually do (every Form step, every data-baker-* event, every outbound destination) with counts and whether it is already counted; that list is where an event key comes from, so never invent one. Then `--event <key> --name 'Booked a call'` to count it. It applies IMMEDIATELY and retroactively \u2014 the whole history is rescored, so a company that has been running for a month gets a month of correct numbers the moment you name the right event. Nothing here is staged and publishing is not involved. Two events given the SAME --name are one row and one number. `--remove <key>` stops counting one. `--flow <slug>` answers the other direction \u2014 what one Form counts, with every ending it has already spelled as an event key, read from the Form itself so it works before a single visitor has seen it. Run it whenever you create or edit a Form: a Form nobody named an outcome for is published converting nobody, and no report says so."
31532
+ description: "What this company counts as a conversion \u2014 the numerator of every conversion rate on Measure. A page event (a Form ending, a data-baker-* event) and an event one of the client's systems sends (a HubSpot event made with `baker analytics sources --create-hubspot`, an order the server posts) both count; a system's event is credited to the last landing its person viewed. A HubSpot event is `page:<its name>` here \u2014 `--event 'page:Deal won' --name 'Deal won'`. Run with no flags first: an empty `definitions` list means every conversion number in every other report is zero \u2014 not because nobody converted, but because nothing is marked as converting, and Baker never guesses. Add `--candidates` to see everything these pages actually do (every Form step, every data-baker-* event, every outbound destination) with counts and whether it is already counted; that list is where an event key comes from, so never invent one. Then `--event <key> --name 'Booked a call'` to count it. Inside a chat a change is STAGED: the person sees it under Conversions in the chat's changes and it applies when they publish, retroactively \u2014 the whole history is rescored, so a company that has been running for a month gets a month of correct numbers. A read shows a proposal marked `staged`. Call an event by its name when you talk to the person, never by its key. Two events given the SAME --name are one row and one number. `--remove <key>` stops counting one. `--flow <slug>` answers the other direction \u2014 what one Form counts, with every ending it has already spelled as an event key, read from the Form itself so it works before a single visitor has seen it. Run it whenever you create or edit a Form: a Form nobody named an outcome for is published converting nobody, and no report says so."
31509
31533
  },
31510
31534
  args: args4,
31511
31535
  run: async ({ args: raw }) => {
@@ -31536,12 +31560,8 @@ var conversionsCommand3 = (() => {
31536
31560
  }
31537
31561
  });
31538
31562
  })();
31539
- var SYSTEM_EVENTS_HINT = "Events your systems send are not listed: a system's event is a funnel step (`baker funnel`) and is sent to an ad platform with `baker analytics sends`, never counted as a page conversion.";
31540
31563
  function pageCandidates(definitions, catalogue, listing) {
31541
- const candidates = catalogue.filter((row) => row.origin !== "systems");
31542
- const hints2 = conversionHints(definitions, candidates, listing);
31543
- if (listing && candidates.length < catalogue.length) hints2.push(SYSTEM_EVENTS_HINT);
31544
- return { candidates, hints: hints2 };
31564
+ return { candidates: [...catalogue], hints: conversionHints(definitions, catalogue, listing) };
31545
31565
  }
31546
31566
  function parsePairs(raw) {
31547
31567
  if (raw === void 0) return void 0;
@@ -45288,12 +45308,37 @@ function splitParts(raw) {
45288
45308
  }
45289
45309
  return parts.map((part) => part.trim()).filter((part) => part.length > 0);
45290
45310
  }
45311
+ var TRANSFORM_REFUSAL = "A value cannot run code. Map the field as it is (firstname=full_name), or split it in the step with a codeExecution that exposes first_name and last_name and map code:first_name.";
45312
+ var METHOD_CALL = /\.\s*[A-Za-z_$][\w$]*\s*\(/;
45313
+ var TRAILING_INDEX = /\[[^\]]*\]$/;
45314
+ function mentionPart(inner) {
45315
+ if (inner.startsWith("tracking.")) return { kind: "tracking", name: inner.slice("tracking.".length) };
45316
+ if (inner.startsWith("global.")) return { kind: "global", name: inner.slice("global.".length) };
45317
+ return referencePart(inner);
45318
+ }
45319
+ function referencePart(ref) {
45320
+ return /\.codeExecution\.[^.]+$/.test(ref) ? { kind: "code", ref } : { kind: "field", ref };
45321
+ }
45291
45322
  function parsePart(part) {
45292
45323
  if (part.startsWith('"') && part.endsWith('"') && part.length >= 2) {
45293
45324
  return { kind: "text", text: part.slice(1, -1) };
45294
45325
  }
45326
+ if (METHOD_CALL.test(part) || TRAILING_INDEX.test(part)) {
45327
+ throw new MappingError(TRANSFORM_REFUSAL);
45328
+ }
45329
+ if (part.startsWith("{{")) {
45330
+ const close = part.indexOf("}}");
45331
+ if (close < 0) {
45332
+ throw new MappingError(
45333
+ `Unclosed \`{{\` in value \`${part}\`.`,
45334
+ "A mention is written {{<stepId>.customForm.<field>}}."
45335
+ );
45336
+ }
45337
+ if (part.slice(close + 2).trim()) throw new MappingError(TRANSFORM_REFUSAL);
45338
+ return mentionPart(part.slice(2, close).trim());
45339
+ }
45295
45340
  const colon = part.indexOf(":");
45296
- if (colon <= 0) return { kind: "field", ref: part };
45341
+ if (colon <= 0) return referencePart(part);
45297
45342
  const prefix = part.slice(0, colon);
45298
45343
  const rest = part.slice(colon + 1).trim();
45299
45344
  if (!PREFIXES.includes(prefix)) {
@@ -45351,7 +45396,12 @@ function fieldOwners(tree) {
45351
45396
  ];
45352
45397
  });
45353
45398
  }
45354
- function splitRef(ref) {
45399
+ function splitRef(ref, segment) {
45400
+ const marker = `.${segment}.`;
45401
+ const at = ref.lastIndexOf(marker);
45402
+ if (at > 0 && !ref.slice(at + marker.length).includes(".")) {
45403
+ return { step: ref.slice(0, at), name: ref.slice(at + marker.length) };
45404
+ }
45355
45405
  const dot = ref.lastIndexOf(".");
45356
45406
  if (dot <= 0) return { name: ref };
45357
45407
  return { step: ref.slice(0, dot), name: ref.slice(dot + 1) };
@@ -45363,7 +45413,7 @@ function titleize(value) {
45363
45413
  return value.split(/[_-]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
45364
45414
  }
45365
45415
  function resolveField(tree, ref) {
45366
- const { step, name } = splitRef(ref);
45416
+ const { step, name } = splitRef(ref, "customForm");
45367
45417
  const owners = fieldOwners(tree);
45368
45418
  const inventory = owners.map((owner2) => ` ${owner2.name} (${owner2.id}): ${owner2.fields.join(", ") || "(no fields)"}`).join("\n");
45369
45419
  const candidates = owners.filter(
@@ -45406,7 +45456,7 @@ function resolveGlobal(tree, name) {
45406
45456
  return mention(id, id);
45407
45457
  }
45408
45458
  function resolveCode(tree, ref) {
45409
- const { step, name } = splitRef(ref);
45459
+ const { step, name } = splitRef(ref, "codeExecution");
45410
45460
  const owners = walkNodes(tree).flatMap((node) => {
45411
45461
  const exposed = externalOf(node).codeExecution?.exposedValues ?? [];
45412
45462
  const names = exposed.map((value) => value?.name).filter((value) => typeof value === "string");
@@ -45950,7 +46000,7 @@ function describeDestination(chosen) {
45950
46000
  // two different questions with two different answers, and the type is the
45951
46001
  // one an agent does not know it is being asked until a mapping is refused.
45952
46002
  ...slots.flatMap((slot) => slot.fieldTypes ? [`${slot.slot} field types \u2014 ${slot.fieldTypes}`] : []),
45953
- 'Values (right of the `=`) are the same for every slot: a form field, tracking:<var>, global:<name>, code:<name>, a "literal", or those joined with +.'
46003
+ 'Values (right of the `=`) are the same for every slot: a form field, tracking:<var>, global:<name>, code:<name>, a "literal", or those joined with +. A field may also be written as the file stores it, {{<stepId>.customForm.<field>}}. A value cannot run code (no .split(), .slice(), [0]).'
45954
46004
  ]
45955
46005
  });
45956
46006
  }