@aglyn/plugins-outreach 1.0.0-beta.221 → 1.0.0-beta.223
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/package.json +14 -14
- package/src/lib/components/do-not-contact-domains.d.ts +6 -2
- package/src/lib/components/do-not-contact-domains.js +73 -11
- package/src/lib/components/do-not-contact-domains.js.map +1 -1
- package/src/lib/components/enrollments-table.d.ts +6 -0
- package/src/lib/components/enrollments-table.js +77 -4
- package/src/lib/components/enrollments-table.js.map +1 -1
- package/src/lib/components/sequence-detail.js +3 -1
- package/src/lib/components/sequence-detail.js.map +1 -1
- package/src/lib/components/sequence-report-card.js +7 -3
- package/src/lib/components/sequence-report-card.js.map +1 -1
- package/src/lib/components/use-outreach-crm.d.ts +18 -15
- package/src/lib/components/use-outreach-crm.js +118 -87
- package/src/lib/components/use-outreach-crm.js.map +1 -1
- package/src/lib/components/use-outreach-data.d.ts +2 -2
- package/src/lib/components/use-outreach-data.js.map +1 -1
- package/src/lib/constants/transfer-resources.d.ts +33 -0
- package/src/lib/constants/transfer-resources.js +38 -0
- package/src/lib/constants/transfer-resources.js.map +1 -0
- package/src/lib/declarations.console-server.d.ts +7 -1
- package/src/lib/declarations.console-server.js +66 -1
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/engine/click-tracking.js +1 -1
- package/src/lib/engine/click-tracking.js.map +1 -1
- package/src/lib/engine/ip-range.d.ts +26 -0
- package/src/lib/engine/ip-range.js +78 -0
- package/src/lib/engine/ip-range.js.map +1 -0
- package/src/lib/engine/open-source.d.ts +24 -0
- package/src/lib/engine/open-source.js +130 -0
- package/src/lib/engine/open-source.js.map +1 -0
- package/src/lib/engine/open-tracking.d.ts +15 -2
- package/src/lib/engine/open-tracking.js +59 -16
- package/src/lib/engine/open-tracking.js.map +1 -1
- package/src/lib/engine/outreach-access.d.ts +47 -0
- package/src/lib/engine/outreach-access.js +56 -0
- package/src/lib/engine/outreach-access.js.map +1 -0
- package/src/lib/enrollment/enroll-people.d.ts +10 -10
- package/src/lib/enrollment/enroll-people.js +30 -25
- package/src/lib/enrollment/enroll-people.js.map +1 -1
- package/src/lib/enrollment/gate-lookups.js +30 -15
- package/src/lib/enrollment/gate-lookups.js.map +1 -1
- package/src/lib/enrollment/message-templates.d.ts +24 -0
- package/src/lib/enrollment/message-templates.js +35 -0
- package/src/lib/enrollment/message-templates.js.map +1 -0
- package/src/lib/model/enrollment-engagement.d.ts +16 -0
- package/src/lib/model/enrollment-engagement.js +16 -0
- package/src/lib/model/enrollment-engagement.js.map +1 -1
- package/src/lib/model/enrollment-history.d.ts +8 -1
- package/src/lib/model/enrollment-history.js +17 -5
- package/src/lib/model/enrollment-history.js.map +1 -1
- package/src/lib/model/enrollment-timeline.d.ts +3 -0
- package/src/lib/model/enrollment-timeline.js +15 -4
- package/src/lib/model/enrollment-timeline.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +12 -3
- package/src/lib/model/outreach.types.js +12 -1
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/sequence-report.d.ts +14 -7
- package/src/lib/model/sequence-report.js +12 -8
- package/src/lib/model/sequence-report.js.map +1 -1
- package/src/lib/plugin.js +18 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/routes/curate-routes.js +4 -6
- package/src/lib/routes/curate-routes.js.map +1 -1
- package/src/lib/routes/enroll-routes.d.ts +0 -12
- package/src/lib/routes/enroll-routes.js +52 -92
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/platform-deps.d.ts +4 -16
- package/src/lib/routes/platform-deps.js +5 -25
- package/src/lib/routes/platform-deps.js.map +1 -1
- package/src/lib/routes/preview-routes.js +25 -13
- package/src/lib/routes/preview-routes.js.map +1 -1
- package/src/lib/routes/register-routes.js +7 -5
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/routes/route-gate.js +6 -17
- package/src/lib/routes/route-gate.js.map +1 -1
- package/src/lib/routes/sequence-routes.d.ts +46 -0
- package/src/lib/routes/sequence-routes.js +155 -63
- package/src/lib/routes/sequence-routes.js.map +1 -1
- package/src/lib/runtime/click-events.js +8 -8
- package/src/lib/runtime/click-events.js.map +1 -1
- package/src/lib/runtime/click-route.js +5 -2
- package/src/lib/runtime/click-route.js.map +1 -1
- package/src/lib/runtime/lead-records.d.ts +18 -10
- package/src/lib/runtime/lead-records.js +14 -60
- package/src/lib/runtime/lead-records.js.map +1 -1
- package/src/lib/runtime/open-events.d.ts +8 -0
- package/src/lib/runtime/open-events.js +10 -3
- package/src/lib/runtime/open-events.js.map +1 -1
- package/src/lib/runtime/platform-runtime-deps.d.ts +9 -5
- package/src/lib/runtime/platform-runtime-deps.js +31 -24
- package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
- package/src/lib/runtime/send-job.js +20 -17
- package/src/lib/runtime/send-job.js.map +1 -1
- package/src/lib/runtime/sync-job.js +10 -5
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/storage/do-not-contact-store.d.ts +8 -0
- package/src/lib/storage/do-not-contact-store.js +13 -0
- package/src/lib/storage/do-not-contact-store.js.map +1 -1
- package/src/lib/testing/stand-in-record-system.d.ts +54 -0
- package/src/lib/testing/stand-in-record-system.js +243 -0
- package/src/lib/testing/stand-in-record-system.js.map +1 -0
- package/src/lib/transfer/do-not-contact-transfer.d.ts +129 -0
- package/src/lib/transfer/do-not-contact-transfer.js +801 -0
- package/src/lib/transfer/do-not-contact-transfer.js.map +1 -0
- package/src/lib/transfer/plan-gate.d.ts +25 -0
- package/src/lib/transfer/plan-gate.js +35 -0
- package/src/lib/transfer/plan-gate.js.map +1 -0
- package/src/lib/transfer/platform-transfer-deps.d.ts +24 -0
- package/src/lib/transfer/platform-transfer-deps.js +39 -0
- package/src/lib/transfer/platform-transfer-deps.js.map +1 -0
- package/src/lib/transfer/sequences-package.d.ts +48 -0
- package/src/lib/transfer/sequences-package.js +93 -0
- package/src/lib/transfer/sequences-package.js.map +1 -0
- package/src/lib/transfer/sequences-package.server.d.ts +31 -0
- package/src/lib/transfer/sequences-package.server.js +207 -0
- package/src/lib/transfer/sequences-package.server.js.map +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/enrollment-history.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { OutreachClickMachineReason } from '../engine/click-tracking'\nimport type { OutreachOpenMachineReason } from '../engine/open-tracking'\nimport type {\n OutreachEnrollmentHistoryEntry,\n OutreachHistoryAction,\n} from './outreach.types'\n\n/**\n * ONE ROW OF AN ENROLLMENT'S HISTORY (AGL-3332), as stored and as read back.\n *\n * Client-safe, beside the other readers, because the click route and the\n * member-action route write these rows on the server and the detail view\n * reads them in the browser, and both must agree on the shape. Read a field\n * at a time: a row that names no time, or no kind this model knows, is\n * dropped rather than shown as something that happened.\n */\n\nconst MACHINE_REASONS: readonly OutreachClickMachineReason[] = ['agent', 'too_soon', 'method']\nconst OPEN_MACHINE_REASONS: readonly OutreachOpenMachineReason[] = [\n 'agent',\n 'too_soon',\n 'method',\n 'image_proxy',\n 'privacy_proxy',\n]\nconst ACTIONS: readonly OutreachHistoryAction[] = ['pause', 'resume', 'stop', 'do_not_contact']\n\n/** A click row as the click route writes it. */\nexport function outreachClickHistoryRow(input: {\n atMs: number\n url: string | null\n stepIndex: number\n human: boolean\n machineReason: OutreachClickMachineReason | null\n}): Record<string, unknown> {\n return {\n kind: 'click',\n atMs: input.atMs,\n url: input.url,\n stepIndex: input.stepIndex,\n human: input.human,\n machineReason: input.human ? null : input.machineReason,\n }\n}\n\n
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/enrollment-history.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { OutreachClickMachineReason } from '../engine/click-tracking'\nimport { OUTREACH_OPEN_SOURCES, type OutreachOpenSource } from '../engine/open-source'\nimport type { OutreachOpenMachineReason } from '../engine/open-tracking'\nimport type {\n OutreachEnrollmentHistoryEntry,\n OutreachHistoryAction,\n} from './outreach.types'\n\n/**\n * ONE ROW OF AN ENROLLMENT'S HISTORY (AGL-3332), as stored and as read back.\n *\n * Client-safe, beside the other readers, because the click route and the\n * member-action route write these rows on the server and the detail view\n * reads them in the browser, and both must agree on the shape. Read a field\n * at a time: a row that names no time, or no kind this model knows, is\n * dropped rather than shown as something that happened.\n */\n\nconst MACHINE_REASONS: readonly OutreachClickMachineReason[] = ['agent', 'too_soon', 'method']\nconst OPEN_MACHINE_REASONS: readonly OutreachOpenMachineReason[] = [\n 'agent',\n 'too_soon',\n 'method',\n 'image_proxy',\n 'privacy_proxy',\n 'scanner',\n]\nconst ACTIONS: readonly OutreachHistoryAction[] = ['pause', 'resume', 'stop', 'do_not_contact']\n\n/** A click row as the click route writes it. */\nexport function outreachClickHistoryRow(input: {\n atMs: number\n url: string | null\n stepIndex: number\n human: boolean\n machineReason: OutreachClickMachineReason | null\n}): Record<string, unknown> {\n return {\n kind: 'click',\n atMs: input.atMs,\n url: input.url,\n stepIndex: input.stepIndex,\n human: input.human,\n machineReason: input.human ? null : input.machineReason,\n }\n}\n\n/**\n * An open row as the short-link route writes it for a tracking image\n * (AGL-3395), with the evidence it was judged on (AGL-3488): the agent the\n * fetch sent, and the network it came from — never its address.\n */\nexport function outreachOpenHistoryRow(input: {\n atMs: number\n stepIndex: number\n human: boolean\n machineReason: OutreachOpenMachineReason | null\n userAgent?: string | null\n source?: OutreachOpenSource | null\n}): Record<string, unknown> {\n return {\n kind: 'open',\n atMs: input.atMs,\n stepIndex: input.stepIndex,\n human: input.human,\n machineReason: input.human ? null : input.machineReason,\n userAgent: input.userAgent ?? null,\n source: input.source ?? null,\n }\n}\n\n/** A member's act as the action route writes it. */\nexport function outreachActionHistoryRow(input: {\n atMs: number\n action: OutreachHistoryAction\n byUid: string\n detail: string | null\n}): Record<string, unknown> {\n const detail = typeof input.detail === 'string' ? input.detail.replace(/\\s+/g, ' ').trim().slice(0, 500) : ''\n return {\n kind: 'action',\n atMs: input.atMs,\n action: input.action,\n byUid: input.byUid,\n detail: detail || null,\n }\n}\n\n/**\n * A row id that sorts by time and cannot collide: two clicks in the same\n * millisecond — a scanner fetching every link at once — are two rows.\n */\nexport function outreachHistoryEntryId(atMs: number, random: () => number = Math.random): string {\n const suffix = Math.floor(random() * 36 ** 6)\n .toString(36)\n .padStart(6, '0')\n return `${String(Math.max(0, Math.floor(atMs))).padStart(15, '0')}-${suffix}`\n}\n\n/** A stored history row in its model shape, or `null` for one nothing can be said about. */\nexport function readOutreachHistoryEntry(\n id: string,\n data: Record<string, unknown> | undefined,\n): OutreachEnrollmentHistoryEntry | null {\n if (!data) return null\n const atMs = data['atMs']\n if (typeof atMs !== 'number' || !Number.isFinite(atMs)) return null\n if (data['kind'] === 'click') {\n const stepIndex = Number(data['stepIndex'])\n const human = data['human'] === true\n const reason = data['machineReason']\n return {\n id,\n kind: 'click',\n atMs,\n url: typeof data['url'] === 'string' && data['url'] ? data['url'] : null,\n stepIndex: Number.isInteger(stepIndex) && stepIndex >= 0 ? stepIndex : 0,\n human,\n machineReason: human\n ? null\n : (MACHINE_REASONS as readonly unknown[]).includes(reason)\n ? (reason as OutreachClickMachineReason)\n : null,\n }\n }\n if (data['kind'] === 'open') {\n const stepIndex = Number(data['stepIndex'])\n const human = data['human'] === true\n const reason = data['machineReason']\n return {\n id,\n kind: 'open',\n atMs,\n stepIndex: Number.isInteger(stepIndex) && stepIndex >= 0 ? stepIndex : 0,\n human,\n machineReason: human\n ? null\n : (OPEN_MACHINE_REASONS as readonly unknown[]).includes(reason)\n ? (reason as OutreachOpenMachineReason)\n : null,\n // Absent on every row from before AGL-3488, which recorded neither.\n userAgent: typeof data['userAgent'] === 'string' && data['userAgent'] ? data['userAgent'] : null,\n source: (OUTREACH_OPEN_SOURCES as readonly unknown[]).includes(data['source'])\n ? (data['source'] as OutreachOpenSource)\n : null,\n }\n }\n if (data['kind'] === 'action') {\n const action = data['action']\n if (!(ACTIONS as readonly unknown[]).includes(action)) return null\n return {\n id,\n kind: 'action',\n atMs,\n action: action as OutreachHistoryAction,\n byUid: typeof data['byUid'] === 'string' ? data['byUid'] : '',\n detail: typeof data['detail'] === 'string' && data['detail'] ? data['detail'] : null,\n }\n }\n return null\n}\n"],"names":["OUTREACH_OPEN_SOURCES","MACHINE_REASONS","OPEN_MACHINE_REASONS","ACTIONS","outreachClickHistoryRow","input","kind","atMs","url","stepIndex","human","machineReason","outreachOpenHistoryRow","userAgent","source","outreachActionHistoryRow","detail","replace","trim","slice","action","byUid","outreachHistoryEntryId","random","Math","suffix","floor","toString","padStart","String","max","readOutreachHistoryEntry","id","data","Number","isFinite","reason","isInteger","includes"],"mappings":"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"}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import type { OutreachClickMachineReason } from '../engine/click-tracking';
|
|
18
18
|
import type { OutreachOpenMachineReason } from '../engine/open-tracking';
|
|
19
|
+
import type { OutreachOpenSource } from '../engine/open-source';
|
|
19
20
|
import { type OutreachMailGateway } from '../engine/mail-gateway';
|
|
20
21
|
import { type OutreachEnrollment, type OutreachEnrollmentHistoryEntry, type OutreachSequenceStep } from './outreach.types';
|
|
21
22
|
/** What kind of thing an entry is, which the view draws an icon for. */
|
|
@@ -60,6 +61,8 @@ export declare function outreachStepName(steps: readonly OutreachSequenceStep[],
|
|
|
60
61
|
export declare const OUTREACH_MACHINE_REASON_LABELS: Record<OutreachClickMachineReason, string>;
|
|
61
62
|
/** Why a fetch of the tracking image was not counted as an open (AGL-3395). */
|
|
62
63
|
export declare const OUTREACH_OPEN_MACHINE_REASON_LABELS: Record<OutreachOpenMachineReason, string>;
|
|
64
|
+
/** Where a fetch of the tracking image came from (AGL-3488), as the timeline says it. */
|
|
65
|
+
export declare const OUTREACH_OPEN_SOURCE_LABELS: Record<OutreachOpenSource, string>;
|
|
63
66
|
/**
|
|
64
67
|
* The timeline, newest first — see the module note.
|
|
65
68
|
*/
|
|
@@ -62,7 +62,16 @@ import { OUTREACH_ENROLLMENT_HISTORY_MAX, OUTREACH_TASK_KIND_LABELS } from "./ou
|
|
|
62
62
|
too_soon: 'it came within 30 seconds of the email arriving, faster than anyone reads',
|
|
63
63
|
method: 'it checked the image without loading it',
|
|
64
64
|
image_proxy: 'Yahoo’s image proxy fetched it, which it does whether or not the person reads it',
|
|
65
|
-
privacy_proxy: 'Apple Mail’s privacy protection fetched it as the email arrived'
|
|
65
|
+
privacy_proxy: 'Apple Mail’s privacy protection fetched it as the email arrived',
|
|
66
|
+
scanner: 'a mail provider’s or a security product’s scanner fetched it, not the person'
|
|
67
|
+
};
|
|
68
|
+
/** Where a fetch of the tracking image came from (AGL-3488), as the timeline says it. */ export const OUTREACH_OPEN_SOURCE_LABELS = {
|
|
69
|
+
google: 'From Google’s network',
|
|
70
|
+
microsoft_filter: 'From Microsoft’s mail filter',
|
|
71
|
+
microsoft: 'From Microsoft’s mail servers',
|
|
72
|
+
apple: 'From Apple’s privacy relay',
|
|
73
|
+
yahoo: 'From Yahoo’s mail proxy',
|
|
74
|
+
other: 'From an address outside the mail providers’ networks'
|
|
66
75
|
};
|
|
67
76
|
const ACTION_TITLES = {
|
|
68
77
|
pause: 'Paused',
|
|
@@ -235,8 +244,9 @@ const byWhom = (uid, memberName)=>uid ? `By ${memberName(uid)}` : null;
|
|
|
235
244
|
atMs: row.atMs,
|
|
236
245
|
title: 'Opened an email',
|
|
237
246
|
facts: [
|
|
238
|
-
|
|
239
|
-
|
|
247
|
+
outreachStepName(steps, row.stepIndex),
|
|
248
|
+
row.source ? OUTREACH_OPEN_SOURCE_LABELS[row.source] : null
|
|
249
|
+
].filter((fact)=>Boolean(fact)),
|
|
240
250
|
tone: 'info'
|
|
241
251
|
} : {
|
|
242
252
|
key: `history-${row.id}`,
|
|
@@ -245,7 +255,8 @@ const byWhom = (uid, memberName)=>uid ? `By ${memberName(uid)}` : null;
|
|
|
245
255
|
title: 'The tracking image was fetched by a machine — not counted',
|
|
246
256
|
facts: [
|
|
247
257
|
outreachStepName(steps, row.stepIndex),
|
|
248
|
-
row.machineReason ? `Read as a machine: ${OUTREACH_OPEN_MACHINE_REASON_LABELS[row.machineReason]}` : null
|
|
258
|
+
row.machineReason ? `Read as a machine: ${OUTREACH_OPEN_MACHINE_REASON_LABELS[row.machineReason]}` : null,
|
|
259
|
+
row.source ? OUTREACH_OPEN_SOURCE_LABELS[row.source] : null
|
|
249
260
|
].filter((fact)=>Boolean(fact)),
|
|
250
261
|
tone: 'default'
|
|
251
262
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/enrollment-timeline.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * EVERYTHING THAT HAPPENED TO ONE PERSON IN ONE SEQUENCE (AGL-3332).\n *\n * The enrollment's detail view is one list, newest first, and this builds\n * it from what is stored — the enrollment itself, and its history rows —\n * without inventing anything that is not:\n *\n * - **Each send** is a step record: the step, the time, the subject as\n * sent, and whether it went out as the person's curated copy.\n * - **Each click** is a history row: the destination, the step whose email\n * carried it, and a person's or a scanner's. Clicks from before the\n * history began exist only as totals, and are ONE row saying so — never\n * a row per click, which would be a fabrication with a timestamp on it.\n * - **Each member's act** — pause, resume, stop, do-not-contact — is a\n * history row with who and why. An enrollment holds only its current\n * stop, so the row is the only record of a pause that was resumed.\n * - **The current stop** — a reply, a bounce, an opt-out, a hold — comes\n * from the enrollment's own fields, with its full reason, unless a\n * history row already says it.\n * - **The gateway hold** (AGL-3326), and its release.\n * - **The enrollment**, with who enrolled them, and each curated step.\n * - **The steps it skipped** (AGL-3228), when it began past the first: one\n * entry naming them, never a \"sent\" — nothing of ours went out for them.\n *\n * Client-safe and free of React: the view renders these entries, and the\n * spec reads them.\n *=========================================*/\n\nimport type { OutreachClickMachineReason } from '../engine/click-tracking'\nimport type { OutreachOpenMachineReason } from '../engine/open-tracking'\nimport { OUTREACH_MAIL_GATEWAY_LABELS, type OutreachMailGateway } from '../engine/mail-gateway'\nimport {\n outreachClickSummary,\n outreachLoggedEngagementRows,\n readOutreachEngagement,\n} from './enrollment-engagement'\nimport {\n OUTREACH_ENROLLMENT_HISTORY_MAX,\n OUTREACH_TASK_KIND_LABELS,\n type OutreachEnrollment,\n type OutreachEnrollmentHistoryEntry,\n type OutreachHistoryAction,\n type OutreachSequenceStep,\n type OutreachStepRecord,\n} from './outreach.types'\n\n/** What kind of thing an entry is, which the view draws an icon for. */\nexport type OutreachTimelineKind =\n | 'enrolled'\n | 'skipped'\n | 'curated'\n | 'sent'\n | 'task'\n | 'click'\n | 'scanner'\n | 'earlier-clicks'\n | 'open'\n | 'machine-open'\n | 'paused'\n | 'resumed'\n | 'stopped'\n | 'do-not-contact'\n | 'replied'\n | 'bounced'\n | 'opted-out'\n | 'held'\n | 'released'\n | 'failed'\n | 'finished'\n\n/** How loudly the view draws an entry. */\nexport type OutreachTimelineTone = 'default' | 'success' | 'info' | 'warning' | 'error'\n\n/** One line of the timeline. */\nexport interface OutreachTimelineEntry {\n /** Stable across renders, for React and for a spec. */\n key: string\n kind: OutreachTimelineKind\n atMs: number\n /** What happened, in a few words. */\n title: string\n /** The one fact that matters most, shown in full: a subject, a reason, a diagnostic. */\n detail?: string | null\n /** The rest, each short: the step, who, a scanner's reason. */\n facts: string[]\n /** A destination the person followed, shown whole and linkable. */\n url?: string | null\n /** What `url` is, when it is not simply \"the link they followed\". */\n urlLabel?: string\n /** The Gmail thread this entry opened, for an \"Open in Gmail\" link. */\n gmailThreadId?: string | null\n tone: OutreachTimelineTone\n}\n\nexport interface OutreachTimelineInput {\n enrollment: OutreachEnrollment\n steps: readonly OutreachSequenceStep[]\n /** The history rows, newest or oldest first; `null` while unread or unreadable. */\n history: readonly OutreachEnrollmentHistoryEntry[] | null\n /** A member's name for a stored uid; the uid itself when the roster does not know it. */\n memberName: (uid: string) => string\n /** A moment as the reader reads it — in the mailbox's zone. */\n formatTime: (ms: number) => string\n /** The mail gateway in front of the person's domain, when it is known. */\n gateway?: OutreachMailGateway | null\n}\n\n/** \"Email 2 · step 3\" for an email step, \"Step 2 · Call\" for a task, \"Step 4\" for one the sequence no longer has. */\nexport function outreachStepName(steps: readonly OutreachSequenceStep[], stepIndex: number): string {\n const step = steps[stepIndex]\n if (step?.kind === 'email') {\n const position = steps.slice(0, stepIndex + 1).filter((entry) => entry.kind === 'email').length\n return `Email ${position} · step ${stepIndex + 1}`\n }\n if (step?.kind === 'task') return `Step ${stepIndex + 1} · ${OUTREACH_TASK_KIND_LABELS[step.taskKind]}`\n return `Step ${stepIndex + 1}`\n}\n\n/** Why a click was read as a scanner's, as the timeline says it. */\nexport const OUTREACH_MACHINE_REASON_LABELS: Record<OutreachClickMachineReason, string> = {\n agent: 'the request named itself as a scanner or a link preview',\n too_soon: 'it came within 30 seconds of the email arriving, faster than anyone reads',\n method: 'it checked the link without opening the page',\n}\n\n/** Why a fetch of the tracking image was not counted as an open (AGL-3395). */\nexport const OUTREACH_OPEN_MACHINE_REASON_LABELS: Record<OutreachOpenMachineReason, string> = {\n agent: 'the request named itself as a scanner or a preview',\n too_soon: 'it came within 30 seconds of the email arriving, faster than anyone reads',\n method: 'it checked the image without loading it',\n image_proxy: 'Yahoo’s image proxy fetched it, which it does whether or not the person reads it',\n privacy_proxy: 'Apple Mail’s privacy protection fetched it as the email arrived',\n}\n\nconst ACTION_TITLES: Record<OutreachHistoryAction, string> = {\n pause: 'Paused',\n resume: 'Resumed',\n stop: 'Stopped by a member',\n do_not_contact: 'Marked do-not-contact',\n}\nconst ACTION_KINDS: Record<OutreachHistoryAction, OutreachTimelineKind> = {\n pause: 'paused',\n resume: 'resumed',\n stop: 'stopped',\n do_not_contact: 'do-not-contact',\n}\n\n/** Which of two entries at the same moment reads first: the outcome above what led to it. */\nconst TIE_ORDER: Record<OutreachTimelineKind, number> = {\n finished: 0,\n replied: 1,\n bounced: 1,\n 'opted-out': 1,\n failed: 1,\n stopped: 2,\n 'do-not-contact': 2,\n paused: 2,\n held: 2,\n released: 3,\n resumed: 3,\n click: 4,\n scanner: 4,\n 'earlier-clicks': 4,\n open: 4,\n 'machine-open': 4,\n task: 5,\n sent: 5,\n curated: 6,\n skipped: 7,\n enrolled: 8,\n}\n\nconst plural = (count: number, one: string, many: string) => `${count} ${count === 1 ? one : many}`\nconst byWhom = (uid: string | null | undefined, memberName: (uid: string) => string) =>\n uid ? `By ${memberName(uid)}` : null\n\n/** The step records that are emails, in the order they went. */\nfunction emailRecords(enrollment: OutreachEnrollment): OutreachStepRecord[] {\n return (enrollment.stepRecords ?? []).filter((record) => record?.kind === 'email')\n}\n\n/**\n * The timeline, newest first — see the module note.\n */\nexport function outreachEnrollmentTimeline(input: OutreachTimelineInput): OutreachTimelineEntry[] {\n const { enrollment, steps, memberName } = input\n const entries: OutreachTimelineEntry[] = []\n const history = input.history ?? []\n const gatewayName = input.gateway ? OUTREACH_MAIL_GATEWAY_LABELS[input.gateway] : null\n\n // ── The enrollment ──────────────────────────────────────────────────────\n if (enrollment.createdAtMs) {\n entries.push({\n key: 'enrolled',\n kind: 'enrolled',\n atMs: enrollment.createdAtMs,\n title: 'Enrolled',\n facts: [\n byWhom(enrollment.enrolledByUid, memberName),\n enrollment.target === 'lead' ? 'As a lead' : 'As a contact',\n enrollment.cold ? 'Cold — confirmed by the member who enrolled them' : null,\n ].filter((fact): fact is string => Boolean(fact)),\n tone: 'default',\n })\n }\n\n // ── Skipped at enrollment (AGL-3228) ───────────────────────────────────\n const skipped = (enrollment.skippedSteps ?? []).filter((entry) => Number.isInteger(entry?.stepIndex))\n if (skipped.length) {\n const start = enrollment.startStepIndex ?? skipped.length\n entries.push({\n key: 'skipped',\n kind: 'skipped',\n atMs: skipped[0].atMs || enrollment.createdAtMs,\n title:\n skipped.length === 1\n ? `Began at step ${start + 1} — step 1 skipped`\n : `Began at step ${start + 1} — steps 1–${skipped.length} skipped`,\n detail: 'Already done by hand before they were enrolled, so the sequence never sent them.',\n facts: skipped.map((entry) => outreachStepName(steps, entry.stepIndex)),\n tone: 'default',\n })\n }\n\n // ── Curated copies (AGL-3324) ───────────────────────────────────────────\n for (const [key, override] of Object.entries(enrollment.stepOverrides ?? {})) {\n if (!override.draftedAtMs) continue\n entries.push({\n key: `curated-${key}`,\n kind: 'curated',\n atMs: override.draftedAtMs,\n title: `Curated ${outreachStepName(steps, Number(key))}`,\n facts: [\n override.source === 'ai' ? (override.edited ? 'AI draft, edited' : 'AI draft') : 'Written by a member',\n byWhom(override.draftedByUid, memberName),\n ].filter((fact): fact is string => Boolean(fact)),\n tone: 'default',\n })\n }\n\n // ── Sends and tasks ─────────────────────────────────────────────────────\n let previousThread: string | null = null\n ;(enrollment.stepRecords ?? []).forEach((record, index) => {\n if (!record || typeof record.atMs !== 'number') return\n if (record.kind === 'task') {\n const step = steps.find((entry) => entry.id === record.stepId) ?? steps[record.stepIndex]\n entries.push({\n key: `task-${index}`,\n kind: 'task',\n atMs: record.atMs,\n title:\n step?.kind === 'task'\n ? `${OUTREACH_TASK_KIND_LABELS[step.taskKind]} task filed`\n : 'Task filed',\n detail: step?.kind === 'task' ? step.title : null,\n facts: [\n outreachStepName(steps, record.stepIndex),\n record.taskId === null ? 'The CRM could not file it' : null,\n ].filter((fact): fact is string => Boolean(fact)),\n tone: 'default',\n })\n return\n }\n const thread = record.gmailThreadId ?? null\n // The link to Gmail sits on the email that STARTED a thread, once.\n const startsThread = Boolean(thread) && thread !== previousThread\n previousThread = thread ?? previousThread\n entries.push({\n key: `sent-${index}`,\n kind: 'sent',\n atMs: record.atMs,\n title: `${outreachStepName(steps, record.stepIndex)} sent`,\n detail: record.subject ? `Subject: ${record.subject}` : null,\n facts: [\n record.curated === 'ai'\n ? 'Curated copy — AI draft'\n : record.curated === 'member'\n ? 'Curated copy — written by a member'\n : null,\n record.links?.length ? plural(record.links.length, 'tracked link', 'tracked links') : null,\n record.openTracked ? 'Tracking image' : null,\n ].filter((fact): fact is string => Boolean(fact)),\n gmailThreadId: startsThread ? thread : null,\n tone: 'default',\n })\n })\n\n // ── Clicks ──────────────────────────────────────────────────────────────\n for (const row of history) {\n if (row.kind !== 'click') continue\n entries.push(\n row.human\n ? {\n key: `history-${row.id}`,\n kind: 'click',\n atMs: row.atMs,\n title: 'Clicked a link',\n url: row.url,\n facts: [`From ${outreachStepName(steps, row.stepIndex)}`],\n tone: 'success',\n }\n : {\n key: `history-${row.id}`,\n kind: 'scanner',\n atMs: row.atMs,\n title: 'A scanner followed a link — not counted',\n url: row.url,\n facts: [\n `From ${outreachStepName(steps, row.stepIndex)}`,\n row.machineReason ? `Read as a scanner: ${OUTREACH_MACHINE_REASON_LABELS[row.machineReason]}` : null,\n ].filter((fact): fact is string => Boolean(fact)),\n tone: 'default',\n },\n )\n }\n\n // ── Opens (AGL-3395) ────────────────────────────────────────────────────\n for (const row of history) {\n if (row.kind !== 'open') continue\n entries.push(\n row.human\n ? {\n key: `history-${row.id}`,\n kind: 'open',\n atMs: row.atMs,\n title: 'Opened an email',\n facts: [`${outreachStepName(steps, row.stepIndex)}`],\n tone: 'info',\n }\n : {\n key: `history-${row.id}`,\n kind: 'machine-open',\n atMs: row.atMs,\n title: 'The tracking image was fetched by a machine — not counted',\n facts: [\n outreachStepName(steps, row.stepIndex),\n row.machineReason ? `Read as a machine: ${OUTREACH_OPEN_MACHINE_REASON_LABELS[row.machineReason]}` : null,\n ].filter((fact): fact is string => Boolean(fact)),\n tone: 'default',\n },\n )\n }\n\n /*\n * Clicks with no row of their own: counted before the per-click history\n * began, or past its limit. ONE entry, placed at the first of them when\n * that is known, saying what the totals hold and nothing more.\n */\n const clicks = outreachClickSummary(enrollment.engagement)\n const engagement = enrollment.engagement\n if (clicks.unloggedClicks > 0 || clicks.unloggedMachineClicks > 0) {\n const everyHumanUnlogged = clicks.unloggedClicks === clicks.clicks\n const loggedRows = outreachLoggedEngagementRows(readOutreachEngagement(enrollment.engagement))\n const facts: string[] = []\n const first = engagement?.firstClickAtMs ?? null\n const last = engagement?.lastClickAtMs ?? null\n if (clicks.unloggedClicks > 0 && first !== null && everyHumanUnlogged && last !== null) {\n facts.push(\n clicks.unloggedClicks === 1\n ? `At ${input.formatTime(first)}`\n : first === last\n ? `First and last at ${input.formatTime(first)}`\n : `First at ${input.formatTime(first)}, last at ${input.formatTime(last)}`,\n )\n } else if (clicks.unloggedClicks > 0 && first !== null) {\n // Later clicks have rows of their own, so only the first is one of these.\n facts.push(`First at ${input.formatTime(first)}`)\n }\n if (clicks.unloggedMachineClicks > 0) {\n facts.push(plural(clicks.unloggedMachineClicks, 'scanner click, not counted', 'scanner clicks, not counted'))\n }\n entries.push({\n key: 'earlier-clicks',\n kind: 'earlier-clicks',\n atMs:\n (clicks.unloggedClicks > 0 ? engagement?.firstClickAtMs : null) ??\n engagement?.lastClickAtMs ??\n emailRecords(enrollment)[0]?.atMs ??\n enrollment.createdAtMs,\n title:\n clicks.unloggedClicks > 0\n ? `${plural(clicks.unloggedClicks, 'earlier click', 'earlier clicks')}, kept as a total`\n : `${plural(clicks.unloggedMachineClicks, 'earlier scanner click', 'earlier scanner clicks')}, kept as a total`,\n detail:\n loggedRows >= OUTREACH_ENROLLMENT_HISTORY_MAX\n ? `Each click is listed on its own up to ${OUTREACH_ENROLLMENT_HISTORY_MAX}; these came before per-click history began, or after this person reached that limit.`\n : 'These came before each click was recorded on its own, so only their count and times were kept.',\n // The last destination is theirs only when every counted click was.\n url: clicks.unloggedClicks > 0 && everyHumanUnlogged ? (engagement?.lastClickUrl ?? null) : null,\n urlLabel: clicks.unloggedClicks === 1 ? 'Link' : 'Last link',\n facts,\n tone: clicks.unloggedClicks > 0 ? 'success' : 'default',\n })\n }\n\n // ── Members' acts ───────────────────────────────────────────────────────\n const actionTimes = new Set<number>()\n for (const row of history) {\n if (row.kind !== 'action') continue\n actionTimes.add(row.atMs)\n const released =\n row.action === 'resume' && enrollment.gatewayHold?.releasedAtMs === row.atMs\n if (released) continue\n entries.push({\n key: `history-${row.id}`,\n kind: ACTION_KINDS[row.action],\n atMs: row.atMs,\n title: ACTION_TITLES[row.action],\n detail: row.detail,\n facts: [byWhom(row.byUid, memberName)].filter((fact): fact is string => Boolean(fact)),\n tone: row.action === 'resume' ? 'default' : row.action === 'pause' ? 'warning' : 'default',\n })\n }\n\n // ── The gateway hold (AGL-3326) ─────────────────────────────────────────\n const hold = enrollment.gatewayHold\n const holdGateway = hold ? OUTREACH_MAIL_GATEWAY_LABELS[hold.gateway] : null\n if (hold?.heldAtMs) {\n entries.push({\n key: 'held',\n kind: 'held',\n atMs: hold.heldAtMs,\n title: `Held — ${holdGateway} refused this sender`,\n detail:\n enrollment.stopReason === 'gateway_blocked_here' && enrollment.stoppedAtMs === hold.heldAtMs\n ? enrollment.stopDetail\n : null,\n facts: ['Nothing was sent to them while it was held'],\n tone: 'warning',\n })\n }\n if (hold?.releasedAtMs) {\n entries.push({\n key: 'released',\n kind: 'released',\n atMs: hold.releasedAtMs,\n title: hold.heldAtMs\n ? `Resumed past the ${holdGateway} hold — sent anyway`\n : `Enrolled past the ${holdGateway} warning`,\n facts: [byWhom(hold.releasedByUid, memberName)].filter((fact): fact is string => Boolean(fact)),\n tone: 'default',\n })\n }\n\n // ── The current stop, from the enrollment ───────────────────────────────\n const stoppedAtMs = enrollment.stoppedAtMs\n const alreadySaid =\n stoppedAtMs !== null &&\n (actionTimes.has(stoppedAtMs) ||\n (enrollment.stopReason === 'gateway_blocked_here' && hold?.heldAtMs === stoppedAtMs))\n if (stoppedAtMs && !alreadySaid && enrollment.status !== 'active' && enrollment.status !== 'finished') {\n const stop = currentStop(enrollment, memberName, gatewayName)\n if (stop) entries.push({ key: 'stop', atMs: stoppedAtMs, ...stop })\n }\n\n // ── Finished ────────────────────────────────────────────────────────────\n if (enrollment.status === 'finished') {\n const last = (enrollment.stepRecords ?? []).at(-1)\n entries.push({\n key: 'finished',\n kind: 'finished',\n atMs: last?.atMs ?? enrollment.updatedAtMs,\n title: enrollment.skippedSteps?.length ? 'Finished — every remaining step ran' : 'Finished — every step ran',\n facts: [],\n tone: 'success',\n })\n }\n\n return entries.sort((a, b) => b.atMs - a.atMs || TIE_ORDER[a.kind] - TIE_ORDER[b.kind])\n}\n\n/** The entry for the stop the enrollment is in now. */\nfunction currentStop(\n enrollment: OutreachEnrollment,\n memberName: (uid: string) => string,\n gatewayName: string | null,\n): Omit<OutreachTimelineEntry, 'key' | 'atMs'> | null {\n const detail = enrollment.stopDetail\n const by = byWhom(enrollment.stoppedByUid, memberName)\n const facts = (...values: Array<string | null>) => values.filter((fact): fact is string => Boolean(fact))\n switch (enrollment.stopReason) {\n case 'reply':\n return {\n kind: 'replied',\n title: 'Replied',\n detail,\n facts: ['The sequence stopped for them'],\n gmailThreadId: enrollment.gmailThreadId ?? enrollment.gmailThreadIds.at(-1) ?? null,\n tone: 'info',\n }\n case 'hard_bounce':\n return {\n kind: 'bounced',\n title: 'Bounced',\n detail,\n facts: facts(gatewayName ? `Their mail gateway: ${gatewayName}` : null, 'The address is on the do-not-contact list'),\n tone: 'error',\n }\n case 'opt_out_reply':\n return { kind: 'opted-out', title: 'Asked not to be emailed, in a reply', detail, facts: [], tone: 'warning' }\n case 'unsubscribe':\n return { kind: 'opted-out', title: 'Unsubscribed', detail, facts: [], tone: 'warning' }\n case 'do_not_contact':\n return { kind: 'do-not-contact', title: 'Put on the do-not-contact list', detail, facts: facts(by), tone: 'warning' }\n case 'manual':\n return enrollment.status === 'paused'\n ? { kind: 'paused', title: 'Paused', detail, facts: facts(by), tone: 'warning' }\n : { kind: 'stopped', title: 'Stopped by a member', detail, facts: facts(by), tone: 'default' }\n case 'gate':\n return { kind: 'stopped', title: 'Stopped — no longer eligible', detail, facts: [], tone: 'default' }\n case 'sequence_archived':\n return { kind: 'stopped', title: 'Stopped — the sequence was archived', detail, facts: [], tone: 'default' }\n case 'gateway_blocked_here':\n return { kind: 'held', title: 'Held — their mail gateway refused this sender', detail, facts: [], tone: 'warning' }\n case 'send_failed':\n return { kind: 'failed', title: 'An email couldn’t be sent', detail, facts: [], tone: 'error' }\n default:\n return null\n }\n}\n\n/** The five figures above the timeline. */\nexport interface OutreachEnrollmentFigures {\n emailsSent: number\n clicks: number\n /** Distinct destinations followed; `atLeast` when only a floor is known. */\n linksFollowed: { count: number; atLeast: boolean }\n scannerClicks: number\n /** Opens judged theirs, and the image fetches that were a machine's (AGL-3395). */\n opens: number\n machineOpens: number\n replies: number\n /** Whether anything has happened to them beyond being enrolled. */\n anything: boolean\n}\n\nexport function outreachEnrollmentFigures(enrollment: OutreachEnrollment): OutreachEnrollmentFigures {\n const clicks = outreachClickSummary(enrollment.engagement)\n const engagement = readOutreachEngagement(enrollment.engagement)\n const emailsSent = emailRecords(enrollment).length\n const replies =\n enrollment.status === 'replied' || enrollment.stopReason === 'reply' || enrollment.stopReason === 'opt_out_reply'\n ? 1\n : 0\n return {\n emailsSent,\n clicks: clicks.clicks,\n linksFollowed:\n clicks.linkCount === null\n ? { count: clicks.followed.length, atLeast: true }\n : { count: clicks.linkCount, atLeast: clicks.linksCapped },\n scannerClicks: clicks.machineClicks,\n opens: engagement.opens,\n machineOpens: engagement.machineOpens,\n replies,\n anything:\n emailsSent > 0 ||\n (enrollment.stepRecords ?? []).length > 0 ||\n clicks.clicks > 0 ||\n clicks.machineClicks > 0 ||\n engagement.opens > 0 ||\n engagement.machineOpens > 0 ||\n enrollment.stoppedAtMs !== null,\n }\n}\n"],"names":["OUTREACH_MAIL_GATEWAY_LABELS","outreachClickSummary","outreachLoggedEngagementRows","readOutreachEngagement","OUTREACH_ENROLLMENT_HISTORY_MAX","OUTREACH_TASK_KIND_LABELS","outreachStepName","steps","stepIndex","step","kind","position","slice","filter","entry","length","taskKind","OUTREACH_MACHINE_REASON_LABELS","agent","too_soon","method","OUTREACH_OPEN_MACHINE_REASON_LABELS","image_proxy","privacy_proxy","ACTION_TITLES","pause","resume","stop","do_not_contact","ACTION_KINDS","TIE_ORDER","finished","replied","bounced","failed","stopped","paused","held","released","resumed","click","scanner","open","task","sent","curated","skipped","enrolled","plural","count","one","many","byWhom","uid","memberName","emailRecords","enrollment","stepRecords","record","outreachEnrollmentTimeline","input","entries","history","gatewayName","gateway","createdAtMs","push","key","atMs","title","facts","enrolledByUid","target","cold","fact","Boolean","tone","skippedSteps","Number","isInteger","start","startStepIndex","detail","map","override","Object","stepOverrides","draftedAtMs","source","edited","draftedByUid","previousThread","forEach","index","find","id","stepId","taskId","thread","gmailThreadId","startsThread","subject","links","openTracked","row","human","url","machineReason","clicks","engagement","unloggedClicks","unloggedMachineClicks","everyHumanUnlogged","loggedRows","first","firstClickAtMs","last","lastClickAtMs","formatTime","lastClickUrl","urlLabel","actionTimes","Set","add","action","gatewayHold","releasedAtMs","byUid","hold","holdGateway","heldAtMs","stopReason","stoppedAtMs","stopDetail","releasedByUid","alreadySaid","has","status","currentStop","at","updatedAtMs","sort","a","b","by","stoppedByUid","values","gmailThreadIds","outreachEnrollmentFigures","emailsSent","replies","linksFollowed","linkCount","followed","atLeast","linksCapped","scannerClicks","machineClicks","opens","machineOpens","anything"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/enrollment-timeline.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * EVERYTHING THAT HAPPENED TO ONE PERSON IN ONE SEQUENCE (AGL-3332).\n *\n * The enrollment's detail view is one list, newest first, and this builds\n * it from what is stored — the enrollment itself, and its history rows —\n * without inventing anything that is not:\n *\n * - **Each send** is a step record: the step, the time, the subject as\n * sent, and whether it went out as the person's curated copy.\n * - **Each click** is a history row: the destination, the step whose email\n * carried it, and a person's or a scanner's. Clicks from before the\n * history began exist only as totals, and are ONE row saying so — never\n * a row per click, which would be a fabrication with a timestamp on it.\n * - **Each member's act** — pause, resume, stop, do-not-contact — is a\n * history row with who and why. An enrollment holds only its current\n * stop, so the row is the only record of a pause that was resumed.\n * - **The current stop** — a reply, a bounce, an opt-out, a hold — comes\n * from the enrollment's own fields, with its full reason, unless a\n * history row already says it.\n * - **The gateway hold** (AGL-3326), and its release.\n * - **The enrollment**, with who enrolled them, and each curated step.\n * - **The steps it skipped** (AGL-3228), when it began past the first: one\n * entry naming them, never a \"sent\" — nothing of ours went out for them.\n *\n * Client-safe and free of React: the view renders these entries, and the\n * spec reads them.\n *=========================================*/\n\nimport type { OutreachClickMachineReason } from '../engine/click-tracking'\nimport type { OutreachOpenMachineReason } from '../engine/open-tracking'\nimport type { OutreachOpenSource } from '../engine/open-source'\nimport { OUTREACH_MAIL_GATEWAY_LABELS, type OutreachMailGateway } from '../engine/mail-gateway'\nimport {\n outreachClickSummary,\n outreachLoggedEngagementRows,\n readOutreachEngagement,\n} from './enrollment-engagement'\nimport {\n OUTREACH_ENROLLMENT_HISTORY_MAX,\n OUTREACH_TASK_KIND_LABELS,\n type OutreachEnrollment,\n type OutreachEnrollmentHistoryEntry,\n type OutreachHistoryAction,\n type OutreachSequenceStep,\n type OutreachStepRecord,\n} from './outreach.types'\n\n/** What kind of thing an entry is, which the view draws an icon for. */\nexport type OutreachTimelineKind =\n | 'enrolled'\n | 'skipped'\n | 'curated'\n | 'sent'\n | 'task'\n | 'click'\n | 'scanner'\n | 'earlier-clicks'\n | 'open'\n | 'machine-open'\n | 'paused'\n | 'resumed'\n | 'stopped'\n | 'do-not-contact'\n | 'replied'\n | 'bounced'\n | 'opted-out'\n | 'held'\n | 'released'\n | 'failed'\n | 'finished'\n\n/** How loudly the view draws an entry. */\nexport type OutreachTimelineTone = 'default' | 'success' | 'info' | 'warning' | 'error'\n\n/** One line of the timeline. */\nexport interface OutreachTimelineEntry {\n /** Stable across renders, for React and for a spec. */\n key: string\n kind: OutreachTimelineKind\n atMs: number\n /** What happened, in a few words. */\n title: string\n /** The one fact that matters most, shown in full: a subject, a reason, a diagnostic. */\n detail?: string | null\n /** The rest, each short: the step, who, a scanner's reason. */\n facts: string[]\n /** A destination the person followed, shown whole and linkable. */\n url?: string | null\n /** What `url` is, when it is not simply \"the link they followed\". */\n urlLabel?: string\n /** The Gmail thread this entry opened, for an \"Open in Gmail\" link. */\n gmailThreadId?: string | null\n tone: OutreachTimelineTone\n}\n\nexport interface OutreachTimelineInput {\n enrollment: OutreachEnrollment\n steps: readonly OutreachSequenceStep[]\n /** The history rows, newest or oldest first; `null` while unread or unreadable. */\n history: readonly OutreachEnrollmentHistoryEntry[] | null\n /** A member's name for a stored uid; the uid itself when the roster does not know it. */\n memberName: (uid: string) => string\n /** A moment as the reader reads it — in the mailbox's zone. */\n formatTime: (ms: number) => string\n /** The mail gateway in front of the person's domain, when it is known. */\n gateway?: OutreachMailGateway | null\n}\n\n/** \"Email 2 · step 3\" for an email step, \"Step 2 · Call\" for a task, \"Step 4\" for one the sequence no longer has. */\nexport function outreachStepName(steps: readonly OutreachSequenceStep[], stepIndex: number): string {\n const step = steps[stepIndex]\n if (step?.kind === 'email') {\n const position = steps.slice(0, stepIndex + 1).filter((entry) => entry.kind === 'email').length\n return `Email ${position} · step ${stepIndex + 1}`\n }\n if (step?.kind === 'task') return `Step ${stepIndex + 1} · ${OUTREACH_TASK_KIND_LABELS[step.taskKind]}`\n return `Step ${stepIndex + 1}`\n}\n\n/** Why a click was read as a scanner's, as the timeline says it. */\nexport const OUTREACH_MACHINE_REASON_LABELS: Record<OutreachClickMachineReason, string> = {\n agent: 'the request named itself as a scanner or a link preview',\n too_soon: 'it came within 30 seconds of the email arriving, faster than anyone reads',\n method: 'it checked the link without opening the page',\n}\n\n/** Why a fetch of the tracking image was not counted as an open (AGL-3395). */\nexport const OUTREACH_OPEN_MACHINE_REASON_LABELS: Record<OutreachOpenMachineReason, string> = {\n agent: 'the request named itself as a scanner or a preview',\n too_soon: 'it came within 30 seconds of the email arriving, faster than anyone reads',\n method: 'it checked the image without loading it',\n image_proxy: 'Yahoo’s image proxy fetched it, which it does whether or not the person reads it',\n privacy_proxy: 'Apple Mail’s privacy protection fetched it as the email arrived',\n scanner: 'a mail provider’s or a security product’s scanner fetched it, not the person',\n}\n\n/** Where a fetch of the tracking image came from (AGL-3488), as the timeline says it. */\nexport const OUTREACH_OPEN_SOURCE_LABELS: Record<OutreachOpenSource, string> = {\n google: 'From Google’s network',\n microsoft_filter: 'From Microsoft’s mail filter',\n microsoft: 'From Microsoft’s mail servers',\n apple: 'From Apple’s privacy relay',\n yahoo: 'From Yahoo’s mail proxy',\n other: 'From an address outside the mail providers’ networks',\n}\n\nconst ACTION_TITLES: Record<OutreachHistoryAction, string> = {\n pause: 'Paused',\n resume: 'Resumed',\n stop: 'Stopped by a member',\n do_not_contact: 'Marked do-not-contact',\n}\nconst ACTION_KINDS: Record<OutreachHistoryAction, OutreachTimelineKind> = {\n pause: 'paused',\n resume: 'resumed',\n stop: 'stopped',\n do_not_contact: 'do-not-contact',\n}\n\n/** Which of two entries at the same moment reads first: the outcome above what led to it. */\nconst TIE_ORDER: Record<OutreachTimelineKind, number> = {\n finished: 0,\n replied: 1,\n bounced: 1,\n 'opted-out': 1,\n failed: 1,\n stopped: 2,\n 'do-not-contact': 2,\n paused: 2,\n held: 2,\n released: 3,\n resumed: 3,\n click: 4,\n scanner: 4,\n 'earlier-clicks': 4,\n open: 4,\n 'machine-open': 4,\n task: 5,\n sent: 5,\n curated: 6,\n skipped: 7,\n enrolled: 8,\n}\n\nconst plural = (count: number, one: string, many: string) => `${count} ${count === 1 ? one : many}`\nconst byWhom = (uid: string | null | undefined, memberName: (uid: string) => string) =>\n uid ? `By ${memberName(uid)}` : null\n\n/** The step records that are emails, in the order they went. */\nfunction emailRecords(enrollment: OutreachEnrollment): OutreachStepRecord[] {\n return (enrollment.stepRecords ?? []).filter((record) => record?.kind === 'email')\n}\n\n/**\n * The timeline, newest first — see the module note.\n */\nexport function outreachEnrollmentTimeline(input: OutreachTimelineInput): OutreachTimelineEntry[] {\n const { enrollment, steps, memberName } = input\n const entries: OutreachTimelineEntry[] = []\n const history = input.history ?? []\n const gatewayName = input.gateway ? OUTREACH_MAIL_GATEWAY_LABELS[input.gateway] : null\n\n // ── The enrollment ──────────────────────────────────────────────────────\n if (enrollment.createdAtMs) {\n entries.push({\n key: 'enrolled',\n kind: 'enrolled',\n atMs: enrollment.createdAtMs,\n title: 'Enrolled',\n facts: [\n byWhom(enrollment.enrolledByUid, memberName),\n enrollment.target === 'lead' ? 'As a lead' : 'As a contact',\n enrollment.cold ? 'Cold — confirmed by the member who enrolled them' : null,\n ].filter((fact): fact is string => Boolean(fact)),\n tone: 'default',\n })\n }\n\n // ── Skipped at enrollment (AGL-3228) ───────────────────────────────────\n const skipped = (enrollment.skippedSteps ?? []).filter((entry) => Number.isInteger(entry?.stepIndex))\n if (skipped.length) {\n const start = enrollment.startStepIndex ?? skipped.length\n entries.push({\n key: 'skipped',\n kind: 'skipped',\n atMs: skipped[0].atMs || enrollment.createdAtMs,\n title:\n skipped.length === 1\n ? `Began at step ${start + 1} — step 1 skipped`\n : `Began at step ${start + 1} — steps 1–${skipped.length} skipped`,\n detail: 'Already done by hand before they were enrolled, so the sequence never sent them.',\n facts: skipped.map((entry) => outreachStepName(steps, entry.stepIndex)),\n tone: 'default',\n })\n }\n\n // ── Curated copies (AGL-3324) ───────────────────────────────────────────\n for (const [key, override] of Object.entries(enrollment.stepOverrides ?? {})) {\n if (!override.draftedAtMs) continue\n entries.push({\n key: `curated-${key}`,\n kind: 'curated',\n atMs: override.draftedAtMs,\n title: `Curated ${outreachStepName(steps, Number(key))}`,\n facts: [\n override.source === 'ai' ? (override.edited ? 'AI draft, edited' : 'AI draft') : 'Written by a member',\n byWhom(override.draftedByUid, memberName),\n ].filter((fact): fact is string => Boolean(fact)),\n tone: 'default',\n })\n }\n\n // ── Sends and tasks ─────────────────────────────────────────────────────\n let previousThread: string | null = null\n ;(enrollment.stepRecords ?? []).forEach((record, index) => {\n if (!record || typeof record.atMs !== 'number') return\n if (record.kind === 'task') {\n const step = steps.find((entry) => entry.id === record.stepId) ?? steps[record.stepIndex]\n entries.push({\n key: `task-${index}`,\n kind: 'task',\n atMs: record.atMs,\n title:\n step?.kind === 'task'\n ? `${OUTREACH_TASK_KIND_LABELS[step.taskKind]} task filed`\n : 'Task filed',\n detail: step?.kind === 'task' ? step.title : null,\n facts: [\n outreachStepName(steps, record.stepIndex),\n record.taskId === null ? 'The CRM could not file it' : null,\n ].filter((fact): fact is string => Boolean(fact)),\n tone: 'default',\n })\n return\n }\n const thread = record.gmailThreadId ?? null\n // The link to Gmail sits on the email that STARTED a thread, once.\n const startsThread = Boolean(thread) && thread !== previousThread\n previousThread = thread ?? previousThread\n entries.push({\n key: `sent-${index}`,\n kind: 'sent',\n atMs: record.atMs,\n title: `${outreachStepName(steps, record.stepIndex)} sent`,\n detail: record.subject ? `Subject: ${record.subject}` : null,\n facts: [\n record.curated === 'ai'\n ? 'Curated copy — AI draft'\n : record.curated === 'member'\n ? 'Curated copy — written by a member'\n : null,\n record.links?.length ? plural(record.links.length, 'tracked link', 'tracked links') : null,\n record.openTracked ? 'Tracking image' : null,\n ].filter((fact): fact is string => Boolean(fact)),\n gmailThreadId: startsThread ? thread : null,\n tone: 'default',\n })\n })\n\n // ── Clicks ──────────────────────────────────────────────────────────────\n for (const row of history) {\n if (row.kind !== 'click') continue\n entries.push(\n row.human\n ? {\n key: `history-${row.id}`,\n kind: 'click',\n atMs: row.atMs,\n title: 'Clicked a link',\n url: row.url,\n facts: [`From ${outreachStepName(steps, row.stepIndex)}`],\n tone: 'success',\n }\n : {\n key: `history-${row.id}`,\n kind: 'scanner',\n atMs: row.atMs,\n title: 'A scanner followed a link — not counted',\n url: row.url,\n facts: [\n `From ${outreachStepName(steps, row.stepIndex)}`,\n row.machineReason ? `Read as a scanner: ${OUTREACH_MACHINE_REASON_LABELS[row.machineReason]}` : null,\n ].filter((fact): fact is string => Boolean(fact)),\n tone: 'default',\n },\n )\n }\n\n // ── Opens (AGL-3395) ────────────────────────────────────────────────────\n for (const row of history) {\n if (row.kind !== 'open') continue\n entries.push(\n row.human\n ? {\n key: `history-${row.id}`,\n kind: 'open',\n atMs: row.atMs,\n title: 'Opened an email',\n facts: [\n outreachStepName(steps, row.stepIndex),\n row.source ? OUTREACH_OPEN_SOURCE_LABELS[row.source] : null,\n ].filter((fact): fact is string => Boolean(fact)),\n tone: 'info',\n }\n : {\n key: `history-${row.id}`,\n kind: 'machine-open',\n atMs: row.atMs,\n title: 'The tracking image was fetched by a machine — not counted',\n facts: [\n outreachStepName(steps, row.stepIndex),\n row.machineReason ? `Read as a machine: ${OUTREACH_OPEN_MACHINE_REASON_LABELS[row.machineReason]}` : null,\n row.source ? OUTREACH_OPEN_SOURCE_LABELS[row.source] : null,\n ].filter((fact): fact is string => Boolean(fact)),\n tone: 'default',\n },\n )\n }\n\n /*\n * Clicks with no row of their own: counted before the per-click history\n * began, or past its limit. ONE entry, placed at the first of them when\n * that is known, saying what the totals hold and nothing more.\n */\n const clicks = outreachClickSummary(enrollment.engagement)\n const engagement = enrollment.engagement\n if (clicks.unloggedClicks > 0 || clicks.unloggedMachineClicks > 0) {\n const everyHumanUnlogged = clicks.unloggedClicks === clicks.clicks\n const loggedRows = outreachLoggedEngagementRows(readOutreachEngagement(enrollment.engagement))\n const facts: string[] = []\n const first = engagement?.firstClickAtMs ?? null\n const last = engagement?.lastClickAtMs ?? null\n if (clicks.unloggedClicks > 0 && first !== null && everyHumanUnlogged && last !== null) {\n facts.push(\n clicks.unloggedClicks === 1\n ? `At ${input.formatTime(first)}`\n : first === last\n ? `First and last at ${input.formatTime(first)}`\n : `First at ${input.formatTime(first)}, last at ${input.formatTime(last)}`,\n )\n } else if (clicks.unloggedClicks > 0 && first !== null) {\n // Later clicks have rows of their own, so only the first is one of these.\n facts.push(`First at ${input.formatTime(first)}`)\n }\n if (clicks.unloggedMachineClicks > 0) {\n facts.push(plural(clicks.unloggedMachineClicks, 'scanner click, not counted', 'scanner clicks, not counted'))\n }\n entries.push({\n key: 'earlier-clicks',\n kind: 'earlier-clicks',\n atMs:\n (clicks.unloggedClicks > 0 ? engagement?.firstClickAtMs : null) ??\n engagement?.lastClickAtMs ??\n emailRecords(enrollment)[0]?.atMs ??\n enrollment.createdAtMs,\n title:\n clicks.unloggedClicks > 0\n ? `${plural(clicks.unloggedClicks, 'earlier click', 'earlier clicks')}, kept as a total`\n : `${plural(clicks.unloggedMachineClicks, 'earlier scanner click', 'earlier scanner clicks')}, kept as a total`,\n detail:\n loggedRows >= OUTREACH_ENROLLMENT_HISTORY_MAX\n ? `Each click is listed on its own up to ${OUTREACH_ENROLLMENT_HISTORY_MAX}; these came before per-click history began, or after this person reached that limit.`\n : 'These came before each click was recorded on its own, so only their count and times were kept.',\n // The last destination is theirs only when every counted click was.\n url: clicks.unloggedClicks > 0 && everyHumanUnlogged ? (engagement?.lastClickUrl ?? null) : null,\n urlLabel: clicks.unloggedClicks === 1 ? 'Link' : 'Last link',\n facts,\n tone: clicks.unloggedClicks > 0 ? 'success' : 'default',\n })\n }\n\n // ── Members' acts ───────────────────────────────────────────────────────\n const actionTimes = new Set<number>()\n for (const row of history) {\n if (row.kind !== 'action') continue\n actionTimes.add(row.atMs)\n const released =\n row.action === 'resume' && enrollment.gatewayHold?.releasedAtMs === row.atMs\n if (released) continue\n entries.push({\n key: `history-${row.id}`,\n kind: ACTION_KINDS[row.action],\n atMs: row.atMs,\n title: ACTION_TITLES[row.action],\n detail: row.detail,\n facts: [byWhom(row.byUid, memberName)].filter((fact): fact is string => Boolean(fact)),\n tone: row.action === 'resume' ? 'default' : row.action === 'pause' ? 'warning' : 'default',\n })\n }\n\n // ── The gateway hold (AGL-3326) ─────────────────────────────────────────\n const hold = enrollment.gatewayHold\n const holdGateway = hold ? OUTREACH_MAIL_GATEWAY_LABELS[hold.gateway] : null\n if (hold?.heldAtMs) {\n entries.push({\n key: 'held',\n kind: 'held',\n atMs: hold.heldAtMs,\n title: `Held — ${holdGateway} refused this sender`,\n detail:\n enrollment.stopReason === 'gateway_blocked_here' && enrollment.stoppedAtMs === hold.heldAtMs\n ? enrollment.stopDetail\n : null,\n facts: ['Nothing was sent to them while it was held'],\n tone: 'warning',\n })\n }\n if (hold?.releasedAtMs) {\n entries.push({\n key: 'released',\n kind: 'released',\n atMs: hold.releasedAtMs,\n title: hold.heldAtMs\n ? `Resumed past the ${holdGateway} hold — sent anyway`\n : `Enrolled past the ${holdGateway} warning`,\n facts: [byWhom(hold.releasedByUid, memberName)].filter((fact): fact is string => Boolean(fact)),\n tone: 'default',\n })\n }\n\n // ── The current stop, from the enrollment ───────────────────────────────\n const stoppedAtMs = enrollment.stoppedAtMs\n const alreadySaid =\n stoppedAtMs !== null &&\n (actionTimes.has(stoppedAtMs) ||\n (enrollment.stopReason === 'gateway_blocked_here' && hold?.heldAtMs === stoppedAtMs))\n if (stoppedAtMs && !alreadySaid && enrollment.status !== 'active' && enrollment.status !== 'finished') {\n const stop = currentStop(enrollment, memberName, gatewayName)\n if (stop) entries.push({ key: 'stop', atMs: stoppedAtMs, ...stop })\n }\n\n // ── Finished ────────────────────────────────────────────────────────────\n if (enrollment.status === 'finished') {\n const last = (enrollment.stepRecords ?? []).at(-1)\n entries.push({\n key: 'finished',\n kind: 'finished',\n atMs: last?.atMs ?? enrollment.updatedAtMs,\n title: enrollment.skippedSteps?.length ? 'Finished — every remaining step ran' : 'Finished — every step ran',\n facts: [],\n tone: 'success',\n })\n }\n\n return entries.sort((a, b) => b.atMs - a.atMs || TIE_ORDER[a.kind] - TIE_ORDER[b.kind])\n}\n\n/** The entry for the stop the enrollment is in now. */\nfunction currentStop(\n enrollment: OutreachEnrollment,\n memberName: (uid: string) => string,\n gatewayName: string | null,\n): Omit<OutreachTimelineEntry, 'key' | 'atMs'> | null {\n const detail = enrollment.stopDetail\n const by = byWhom(enrollment.stoppedByUid, memberName)\n const facts = (...values: Array<string | null>) => values.filter((fact): fact is string => Boolean(fact))\n switch (enrollment.stopReason) {\n case 'reply':\n return {\n kind: 'replied',\n title: 'Replied',\n detail,\n facts: ['The sequence stopped for them'],\n gmailThreadId: enrollment.gmailThreadId ?? enrollment.gmailThreadIds.at(-1) ?? null,\n tone: 'info',\n }\n case 'hard_bounce':\n return {\n kind: 'bounced',\n title: 'Bounced',\n detail,\n facts: facts(gatewayName ? `Their mail gateway: ${gatewayName}` : null, 'The address is on the do-not-contact list'),\n tone: 'error',\n }\n case 'opt_out_reply':\n return { kind: 'opted-out', title: 'Asked not to be emailed, in a reply', detail, facts: [], tone: 'warning' }\n case 'unsubscribe':\n return { kind: 'opted-out', title: 'Unsubscribed', detail, facts: [], tone: 'warning' }\n case 'do_not_contact':\n return { kind: 'do-not-contact', title: 'Put on the do-not-contact list', detail, facts: facts(by), tone: 'warning' }\n case 'manual':\n return enrollment.status === 'paused'\n ? { kind: 'paused', title: 'Paused', detail, facts: facts(by), tone: 'warning' }\n : { kind: 'stopped', title: 'Stopped by a member', detail, facts: facts(by), tone: 'default' }\n case 'gate':\n return { kind: 'stopped', title: 'Stopped — no longer eligible', detail, facts: [], tone: 'default' }\n case 'sequence_archived':\n return { kind: 'stopped', title: 'Stopped — the sequence was archived', detail, facts: [], tone: 'default' }\n case 'gateway_blocked_here':\n return { kind: 'held', title: 'Held — their mail gateway refused this sender', detail, facts: [], tone: 'warning' }\n case 'send_failed':\n return { kind: 'failed', title: 'An email couldn’t be sent', detail, facts: [], tone: 'error' }\n default:\n return null\n }\n}\n\n/** The five figures above the timeline. */\nexport interface OutreachEnrollmentFigures {\n emailsSent: number\n clicks: number\n /** Distinct destinations followed; `atLeast` when only a floor is known. */\n linksFollowed: { count: number; atLeast: boolean }\n scannerClicks: number\n /** Opens judged theirs, and the image fetches that were a machine's (AGL-3395). */\n opens: number\n machineOpens: number\n replies: number\n /** Whether anything has happened to them beyond being enrolled. */\n anything: boolean\n}\n\nexport function outreachEnrollmentFigures(enrollment: OutreachEnrollment): OutreachEnrollmentFigures {\n const clicks = outreachClickSummary(enrollment.engagement)\n const engagement = readOutreachEngagement(enrollment.engagement)\n const emailsSent = emailRecords(enrollment).length\n const replies =\n enrollment.status === 'replied' || enrollment.stopReason === 'reply' || enrollment.stopReason === 'opt_out_reply'\n ? 1\n : 0\n return {\n emailsSent,\n clicks: clicks.clicks,\n linksFollowed:\n clicks.linkCount === null\n ? { count: clicks.followed.length, atLeast: true }\n : { count: clicks.linkCount, atLeast: clicks.linksCapped },\n scannerClicks: clicks.machineClicks,\n opens: engagement.opens,\n machineOpens: engagement.machineOpens,\n replies,\n anything:\n emailsSent > 0 ||\n (enrollment.stepRecords ?? []).length > 0 ||\n clicks.clicks > 0 ||\n clicks.machineClicks > 0 ||\n engagement.opens > 0 ||\n engagement.machineOpens > 0 ||\n enrollment.stoppedAtMs !== null,\n }\n}\n"],"names":["OUTREACH_MAIL_GATEWAY_LABELS","outreachClickSummary","outreachLoggedEngagementRows","readOutreachEngagement","OUTREACH_ENROLLMENT_HISTORY_MAX","OUTREACH_TASK_KIND_LABELS","outreachStepName","steps","stepIndex","step","kind","position","slice","filter","entry","length","taskKind","OUTREACH_MACHINE_REASON_LABELS","agent","too_soon","method","OUTREACH_OPEN_MACHINE_REASON_LABELS","image_proxy","privacy_proxy","scanner","OUTREACH_OPEN_SOURCE_LABELS","google","microsoft_filter","microsoft","apple","yahoo","other","ACTION_TITLES","pause","resume","stop","do_not_contact","ACTION_KINDS","TIE_ORDER","finished","replied","bounced","failed","stopped","paused","held","released","resumed","click","open","task","sent","curated","skipped","enrolled","plural","count","one","many","byWhom","uid","memberName","emailRecords","enrollment","stepRecords","record","outreachEnrollmentTimeline","input","entries","history","gatewayName","gateway","createdAtMs","push","key","atMs","title","facts","enrolledByUid","target","cold","fact","Boolean","tone","skippedSteps","Number","isInteger","start","startStepIndex","detail","map","override","Object","stepOverrides","draftedAtMs","source","edited","draftedByUid","previousThread","forEach","index","find","id","stepId","taskId","thread","gmailThreadId","startsThread","subject","links","openTracked","row","human","url","machineReason","clicks","engagement","unloggedClicks","unloggedMachineClicks","everyHumanUnlogged","loggedRows","first","firstClickAtMs","last","lastClickAtMs","formatTime","lastClickUrl","urlLabel","actionTimes","Set","add","action","gatewayHold","releasedAtMs","byUid","hold","holdGateway","heldAtMs","stopReason","stoppedAtMs","stopDetail","releasedByUid","alreadySaid","has","status","currentStop","at","updatedAtMs","sort","a","b","by","stoppedByUid","values","gmailThreadIds","outreachEnrollmentFigures","emailsSent","replies","linksFollowed","linkCount","followed","atLeast","linksCapped","scannerClicks","machineClicks","opens","machineOpens","anything"],"mappings":"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"}
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
*/
|
|
37
37
|
import type { OutreachClickMachineReason } from '../engine/click-tracking';
|
|
38
38
|
import type { OutreachOpenMachineReason } from '../engine/open-tracking';
|
|
39
|
+
import type { OutreachOpenSource } from '../engine/open-source';
|
|
39
40
|
import type { OutreachGatewayDayCounts, OutreachMailGateway } from '../engine/mail-gateway';
|
|
40
41
|
/**
|
|
41
42
|
* Where Outreach keeps its records.
|
|
@@ -503,13 +504,13 @@ export interface OutreachEnrollmentEngagement {
|
|
|
503
504
|
/** The first, which is what makes them one of the sequence's `uniqueClicks`. */
|
|
504
505
|
firstClickAtMs: number | null;
|
|
505
506
|
lastClickAtMs: number | null;
|
|
506
|
-
/** The destination they followed last, as `
|
|
507
|
+
/** The destination they followed last, as `sendLinkKey` reduces it. */
|
|
507
508
|
lastClickUrl: string | null;
|
|
508
509
|
/** Clicks on this person's links that were a machine's. */
|
|
509
510
|
machineClicks: number;
|
|
510
511
|
/**
|
|
511
512
|
* Every distinct destination this person followed since the per-click
|
|
512
|
-
* history began (AGL-3332), as `
|
|
513
|
+
* history began (AGL-3332), as `sendLinkKey` reduces it, oldest first
|
|
513
514
|
* and at most {@link OUTREACH_ENGAGEMENT_LINKS_MAX}. What the table counts
|
|
514
515
|
* as "links" and filters "followed this link" by, without a read of the
|
|
515
516
|
* history. A click from before the history kept only `lastClickUrl`.
|
|
@@ -555,7 +556,7 @@ export interface OutreachClickHistoryEntry {
|
|
|
555
556
|
id: string;
|
|
556
557
|
kind: 'click';
|
|
557
558
|
atMs: number;
|
|
558
|
-
/** The destination as `
|
|
559
|
+
/** The destination as `sendLinkKey` reduces it — the link rollup's key; `null` when unreadable. */
|
|
559
560
|
url: string | null;
|
|
560
561
|
/** The step whose email carried the link. */
|
|
561
562
|
stepIndex: number;
|
|
@@ -585,6 +586,10 @@ export interface OutreachOpenHistoryEntry {
|
|
|
585
586
|
human: boolean;
|
|
586
587
|
/** Why it was read as a machine's; `null` for a person's. */
|
|
587
588
|
machineReason: OutreachOpenMachineReason | null;
|
|
589
|
+
/** The agent the fetch sent (AGL-3488); `null` on rows from before it was kept. */
|
|
590
|
+
userAgent?: string | null;
|
|
591
|
+
/** The network the fetch came from (AGL-3488); `null` when unknown or not kept. */
|
|
592
|
+
source?: OutreachOpenSource | null;
|
|
588
593
|
}
|
|
589
594
|
export type OutreachEnrollmentHistoryEntry = OutreachClickHistoryEntry | OutreachOpenHistoryEntry | OutreachActionHistoryEntry;
|
|
590
595
|
/** An ordered set of steps sent from one mailbox (`orgs/{orgId}/outreachSequences/{id}`). */
|
|
@@ -966,9 +971,13 @@ export interface OutreachComplianceSettingsDocument extends OutreachComplianceSe
|
|
|
966
971
|
/** Why an address is on the list. */
|
|
967
972
|
export declare const OUTREACH_DO_NOT_CONTACT_REASONS: readonly ["manual", "opt_out_reply", "unsubscribe", "hard_bounce", "gateway_block"];
|
|
968
973
|
export type OutreachDoNotContactReason = (typeof OUTREACH_DO_NOT_CONTACT_REASONS)[number];
|
|
974
|
+
/** Why an entry is on the list, as the Compliance page and an export say it. */
|
|
975
|
+
export declare const OUTREACH_DO_NOT_CONTACT_REASON_LABELS: Record<OutreachDoNotContactReason, string>;
|
|
969
976
|
/** What put an address on the list: a member, or the sending runtime. */
|
|
970
977
|
export declare const OUTREACH_DO_NOT_CONTACT_SOURCES: readonly ["member", "runtime"];
|
|
971
978
|
export type OutreachDoNotContactSource = (typeof OUTREACH_DO_NOT_CONTACT_SOURCES)[number];
|
|
979
|
+
/** Who added an entry, as an export says it. */
|
|
980
|
+
export declare const OUTREACH_DO_NOT_CONTACT_SOURCE_LABELS: Record<OutreachDoNotContactSource, string>;
|
|
972
981
|
/** One address on the list (`orgs/{orgId}/outreachDoNotContact/{key}`). */
|
|
973
982
|
export interface OutreachDoNotContactEntry {
|
|
974
983
|
/** `outreachDoNotContactKey` of the address, which is also the document id. */
|
|
@@ -153,7 +153,7 @@ export const OUTREACH_TASK_KIND_LABELS = {
|
|
|
153
153
|
* loaded would report the first page's numbers as the sequence's.
|
|
154
154
|
*
|
|
155
155
|
* Every field is optional and every absence means "not recorded", never
|
|
156
|
-
* zero. The distinction is the whole of `
|
|
156
|
+
* zero. The distinction is the whole of `send-report.ts`'s honesty and
|
|
157
157
|
* it is kept here for the same reason: a sequence that ran before this
|
|
158
158
|
* existed has no counters, and reporting its click rate as 0% would publish
|
|
159
159
|
* a fact about our schema as a fact about its recipients.
|
|
@@ -332,9 +332,20 @@ export const OUTREACH_TASK_KIND_LABELS = {
|
|
|
332
332
|
* a verdict on the whole domain, and is filed against it.
|
|
333
333
|
*/ 'gateway_block'
|
|
334
334
|
];
|
|
335
|
+
/** Why an entry is on the list, as the Compliance page and an export say it. */ export const OUTREACH_DO_NOT_CONTACT_REASON_LABELS = {
|
|
336
|
+
manual: 'Added by a member',
|
|
337
|
+
opt_out_reply: 'A reply asked not to be emailed',
|
|
338
|
+
unsubscribe: 'Unsubscribed',
|
|
339
|
+
hard_bounce: 'Mail bounced',
|
|
340
|
+
gateway_block: 'Its mail gateway blocked the sender'
|
|
341
|
+
};
|
|
335
342
|
/** What put an address on the list: a member, or the sending runtime. */ export const OUTREACH_DO_NOT_CONTACT_SOURCES = [
|
|
336
343
|
'member',
|
|
337
344
|
'runtime'
|
|
338
345
|
];
|
|
346
|
+
/** Who added an entry, as an export says it. */ export const OUTREACH_DO_NOT_CONTACT_SOURCE_LABELS = {
|
|
347
|
+
member: 'A member',
|
|
348
|
+
runtime: 'Sequences, automatically'
|
|
349
|
+
};
|
|
339
350
|
|
|
340
351
|
//# sourceMappingURL=outreach.types.js.map
|