PageSourceSearch

https://invoicecraftly.com/_assets/js/features/product-events/product-events.9f4bf1fc5936.js

js invoicecraftly.com collected 2026-09-24 14:46:33 UTC 17,977 bytes, 455 lines download raw bytes

1import {
2  APPROVED_TEMPLATE_IDS,
3  SUPPORTED_DOCUMENT_TYPES
4} from '../editor-launch/launch-intent.6b5799d44715.js';
5import {
6  COMMERCIAL_CAPABILITY_ID,
7  COMMERCIAL_PLAN_ID
8} from '../commercial/capability-registry.e6c305e45094.js';
9import { PRIMARY_DOCUMENT_ACTIONS } from '../workspace-domain/primary-action-resolver.54183edd13be.js';
10import { PRIVATE_WORK_OUTCOME_CODE } from '../invoice-route-advisor/private-work/outcome.6265b96c3387.js';
11import { PROFESSION_PRESET_IDS } from '../profession-presets/profession-preset-registry.41522df30948.js';
12
13const ENUMS = Object.freeze({
14  documentType: Object.freeze([...SUPPORTED_DOCUMENT_TYPES]),
15  launchMode: Object.freeze(['blank', 'template', 'draft', 'duplicate', 'derive-invoice', 'resume', 'profession-preset']),
16  templateId: Object.freeze([...APPROVED_TEMPLATE_IDS]),
17  exportFormat: Object.freeze(['pdf', 'png', 'svg', 'csv']),
18  pageFitState: Object.freeze(['checking', 'fits', 'near', 'overflow']),
19  layoutClass: Object.freeze(['ledger', 'banner', 'postmark']),
20  repeatMode: Object.freeze(['duplicate', 'next', 'derive-invoice']),
21  publicTarget: Object.freeze(['editor', 'templates', 'guide', 'privacy', 'contact', 'feedback']),
22  extensionStore: Object.freeze(['chrome', 'edge']),
23  extensionStorePage: Object.freeze(['chrome-extension', 'edge-extension']),
24  personalCloudProvider: Object.freeze(['dropbox', 'google-drive', 'onedrive']),
25  personalCloudErrorCategory: Object.freeze([
26    'authorization',
27    'network',
28    'quota',
29    'not-found',
30    'validation',
31    'provider',
32    'unsupported',
33    'unknown'
34  ]),
35  personalCloudSurface: Object.freeze(['personal-cloud-card', 'personal-cloud-history-dialog']),
36  personalCloudEntrySurface: Object.freeze(['workspace-overview', 'document-library']),
37  personalCloudStage: Object.freeze(['dogfood', 'beta']),
38  companyLookupCountry: Object.freeze(['NO', 'FI', 'SE', 'DK', 'IS']),
39  companyLookupSurface: Object.freeze(['standalone', 'editor']),
40  companyLookupOutcome: Object.freeze(['success', 'empty']),
41  companyLookupResultCountBucket: Object.freeze(['0', '1', '2-4', '5-8', '9+']),
42  companyLookupErrorClass: Object.freeze(['invalid-query', 'not-found', 'rate-limited', 'provider', 'unknown']),
43  privateWorkCountry: Object.freeze(['DK', 'NO', 'SE', 'FI']),
44  privateWorkSurface: Object.freeze(['guide-checker']),
45  privateWorkOutcome: Object.freeze(Object.values(PRIVATE_WORK_OUTCOME_CODE)),
46  privateWorkDestination: Object.freeze(['editor', 'official-guidance', 'external-service', 'business-registration', 'none']),
47  reusableClientSurface: Object.freeze(['editor']),
48  reusableClientSaveMode: Object.freeze(['create', 'update']),
49  reusableItemSurface: Object.freeze(['editor']),
50  reusableItemSaveMode: Object.freeze(['create', 'update']),
51  primaryDocumentAction: Object.freeze([...PRIMARY_DOCUMENT_ACTIONS]),
52  professionPresetId: Object.freeze([...PROFESSION_PRESET_IDS]),
53  commercialCapability: Object.freeze(Object.values(COMMERCIAL_CAPABILITY_ID)),
54  commercialPlan: Object.freeze(Object.values(COMMERCIAL_PLAN_ID)),
55  commercialSurface: Object.freeze([
56    'pricing',
57    'editor',
58    'workspace',
59    'personal-cloud',
60    'post-export',
61    'service-page'
62  ]),
63  errorCode: Object.freeze([
64    'storage-unavailable',
65    'storage-quota',
66    'draft-not-found',
67    'migration-failed',
68    'asset-missing',
69    'qr-invalid',
70    'export-validation',
71    'export-overflow',
72    'export-failed',
73    'download-blocked',
74    'backup-invalid',
75    'backup-unsupported'
76  ])
77});
78
79const requiredEnum = (enumName) => Object.freeze({ type: 'enum', enumName, required: true });
80const optionalEnum = (enumName) => Object.freeze({ type: 'enum', enumName, required: false });
81const requiredBoolean = Object.freeze({ type: 'boolean', required: true });
82
83export const PRODUCT_EVENT_DEFINITIONS = Object.freeze({
84  landing_start_selected: Object.freeze({
85    documentType: requiredEnum('documentType'),
86    launchMode: requiredEnum('launchMode'),
87    templateId: optionalEnum('templateId'),
88    returningLocalUser: requiredBoolean
89  }),
90  editor_opened: Object.freeze({
91    documentType: requiredEnum('documentType'),
92    launchMode: requiredEnum('launchMode'),
93    templateId: optionalEnum('templateId'),
94    professionPresetId: optionalEnum('professionPresetId'),
95    returningLocalUser: requiredBoolean,
96    success: requiredBoolean
97  }),
98  editor_ready: Object.freeze({
99    success: requiredBoolean
100  }),
101  extension_handoff_imported: Object.freeze({
102    success: requiredBoolean
103  }),
104  edge_extension_handoff_imported: Object.freeze({
105    success: requiredBoolean
106  }),
107  extension_store_cta_clicked: Object.freeze({
108    store: requiredEnum('extensionStore'),
109    page: requiredEnum('extensionStorePage')
110  }),
111  invoice_first_edit: Object.freeze({
112    success: requiredBoolean
113  }),
114  return_use: Object.freeze({
115    success: requiredBoolean
116  }),
117  local_overview_opened: Object.freeze({
118    success: requiredBoolean
119  }),
120  starting_point_selected: Object.freeze({
121    documentType: requiredEnum('documentType'),
122    templateId: requiredEnum('templateId')
123  }),
124  layout_selected: Object.freeze({
125    documentType: requiredEnum('documentType'),
126    layoutClass: requiredEnum('layoutClass')
127  }),
128  first_item_added: Object.freeze({
129    documentType: requiredEnum('documentType')
130  }),
131  preview_opened: Object.freeze({
132    documentType: requiredEnum('documentType'),
133    layoutClass: requiredEnum('layoutClass')
134  }),
135  page_fit_changed: Object.freeze({
136    documentType: requiredEnum('documentType'),
137    layoutClass: requiredEnum('layoutClass'),
138    pageFitState: requiredEnum('pageFitState')
139  }),
140  export_started: Object.freeze({
141    documentType: requiredEnum('documentType'),
142    exportFormat: requiredEnum('exportFormat'),
143    pageFitState: requiredEnum('pageFitState')
144  }),
145  export_completed: Object.freeze({
146    documentType: requiredEnum('documentType'),
147    exportFormat: requiredEnum('exportFormat'),
148    pageFitState: requiredEnum('pageFitState'),
149    success: requiredBoolean
150  }),
151  export_failed: Object.freeze({
152    documentType: requiredEnum('documentType'),
153    exportFormat: requiredEnum('exportFormat'),
154    pageFitState: requiredEnum('pageFitState'),
155    errorCode: requiredEnum('errorCode'),
156    success: requiredBoolean
157  }),
158  document_resumed: Object.freeze({
159    documentType: requiredEnum('documentType'),
160    success: requiredBoolean
161  }),
162  repeat_document_created: Object.freeze({
163    documentType: requiredEnum('documentType'),
164    repeatMode: requiredEnum('repeatMode'),
165    success: requiredBoolean
166  }),
167  // JTD-1 reusable Client acceleration. Structural-only per
168  
168// context/skills/initiative-job-to-document-workflow-acceleration/SKILL.md
169  // — never client name/email/address/taxId.
170  reusable_client_selected: Object.freeze({
171    surface: requiredEnum('reusableClientSurface')
172  }),
173  reusable_client_saved: Object.freeze({
174    surface: requiredEnum('reusableClientSurface'),
175    saveMode: requiredEnum('reusableClientSaveMode')
176  }),
177  // JTD-2 Saved Services/Items autocomplete. Structural-only per
178  
178// context/skills/initiative-job-to-document-workflow-acceleration/SKILL.md
179  // — never item description/price/tax content.
180  reusable_item_selected: Object.freeze({
181    surface: requiredEnum('reusableItemSurface')
182  }),
183  reusable_item_saved: Object.freeze({
184    surface: requiredEnum('reusableItemSurface'),
185    saveMode: requiredEnum('reusableItemSaveMode')
186  }),
187  // JTD-3 contextual primary-action resolver. Structural-only per
188  
188// context/specs/initiative-job-to-document-workflow-build-package-index.md
189  // Package JTD-3 — never document id/number/client/amount/content.
190  contextual_action_used: Object.freeze({
191    action: requiredEnum('primaryDocumentAction')
192  }),
193  defaults_saved: Object.freeze({
194    documentType: requiredEnum('documentType'),
195    success: requiredBoolean
196  }),
197  backup_exported: Object.freeze({
198    success: requiredBoolean
199  }),
200  backup_imported: Object.freeze({
201    success: requiredBoolean,
202    errorCode: optionalEnum('errorCode')
203  }),
204  public_content_cta_selected: Object.freeze({
205    publicTarget: requiredEnum('publicTarget')
206  }),
207  // NPW-5 private-work decision funnel. Closed categorical fields only —
208  // never identifiers, names, amounts, work descriptions, invoice contents or free text.
209  private_work_entry_viewed: Object.freeze({
210    country: requiredEnum('privateWorkCountry'),
211    surface: requiredEnum('privateWorkSurface')
212  }),
213  private_work_checker_started: Object.freeze({
214    country: requiredEnum('privateWorkCountry'),
215    surface: requiredEnum('privateWorkSurface')
216  }),
217  private_work_checker_completed: Object.freeze({
218    country: requiredEnum('privateWorkCountry'),
219    surface: requiredEnum('privateWorkSurface'),
220    outcomeCode: requiredEnum('privateWorkOutcome')
221  }),
222  private_work_outcome_shown: Object.freeze({
223    country: requiredEnum('privateWorkCountry'),
224    surface: requiredEnum('privateWorkSurface'),
225    outcomeCode: requiredEnum('privateWorkOutcome'),
226    destinationKind: requiredEnum('privateWorkDestination')
227  }),
228  private_work_official_source_opened: Object.freeze({
229    country: requiredEnum('privateWorkCountry'),
230    outcomeCode: requiredEnum('privateWorkOutcome')
231  }),
232  private_work_editor_handoff_started: Object.freeze({
233    country: requiredEnum('privateWorkCountry'),
234    outcomeCode: requiredEnum('privateWorkOutcome')
235  }),
236  // Feature 89G company-lookup funnel. The closed schema deliberately allows only coarse
237  // country/surface/outcome categories. Search text, company names, registration numbers,
238  // addresses and provider payload fields are unsupported by construction.
239  company_lookup_view: Object.freeze({
240    country: requiredEnum('companyLookupCountry'),
241    surface: requiredEnum('companyLookupSurface')
242  }),
243  company_lookup_search: Object.freeze({
244    country: requiredEnum('companyLookupCountry'),
245    surface: requiredEnum('companyLookupSurface')
246  }),
247  company_lookup_results: Object.freeze({
248    country: requiredEnum('companyLookupCountry'),
249    surface: requiredEnum('companyLookupSurface'),
250    outcome: requiredEnum('companyLookupOutcome'),
251    resultCountBucket: requiredEnum('companyLookupResultCountBucket')
252  }),
253  company_lookup_select: Object.freeze({
254    country: requiredEnum('companyLookupCountry'),
255    surface: requiredEnum('companyLookupSurface')
256  }),
257  company_lookup_start_invoice: Object.freeze({
258    country: requiredEnum('companyLookupCountry'),
259    surface: requiredEnum('companyLookupSurface')
260  }),
261  company_lookup_error: Object.freeze({
262    country: requiredEnum('companyLookupCountry'),
263    surface: requiredEnum('companyLookupSurface'),
264    errorClass: requiredEnum('companyLookupErrorClass')
265  }),
266  // Feature 81A defines these privacy-safe commercial events before any public pricing or
267  // checkout flow exists. Only closed categorical product context and booleans are accepted.
268  pricing_viewed: Object.freeze({
269    planId: requiredEnum('commercialPlan'),
270    surface: requiredEnum('commercialSurface')
271  }),
272  pro_feature_discovered: Object.freeze({
273    capabilityId: requiredEnum('commercialCapability'),
274    surface: requiredEnum('commercialSurface')
275  }),
276  upgrade_cta_clicked: Object.freeze({
277    capabilityId: requiredEnum('commercialCapability'),
278    planId: requiredEnum('commercialPlan'),
279    surface: requiredEnum('commercialSurface')
280  }),
281  checkout_started: Object.freeze({
282    planId: requiredEnum('commercialPlan'),
283    surface: requiredEnum('commercialSurface')
284  }),
285  checkout_completed: Object.freeze({
286    planId: requiredEnum('commercialPlan'),
287    surface: requiredEnum('commercialSurface'),
288    success: requiredBoolean
289  }),
290  entitlement_activated: Object.freeze({
291    planId: requiredEnum('commercialPlan'),
292    success: requiredBoolean
293  }),
294  pro_feature_used: Object.freeze({
295    capabilityId: requiredEnum('commercialCapability'),
296    success: requiredBoolean
297  }),
298  // Feature 82/88 surface exposure and Personal Cloud operational funnel. Properties are
299  // deliberately limited to closed enums/booleans — never invoice/client/business content,
300  // amounts, snapshot hashes, artifact IDs, filenames or provider paths. These events are emitted
301  // only through the existing consent-gated first-party sink on the authorized Workspace surfaces.
302  workspace_overview_viewed: Object.freeze({}),
303  document_library_viewed: Object.freeze({}),
304  personal_cloud_card_viewed: Object.freeze({
305    surface: requiredEnum('personalCloudEntrySurface'),
306    stage: optionalEnum('personalCloudStage')
307  }),
308  personal_cloud_connect_started: Object.freeze({
309    provider: requiredEnum('personalCloudProvider'),
310    stage: optionalEnum('personalCloudStage')
311  }),
312  personal_cloud_connected: Object.freeze({
313    provider: requiredEnum('personalCloudProvider'),
314    success: requiredBoolean,
315    stage: optionalEnum('personalCloudStage')
316  }),
317  personal_cloud_backup_started: Object.freeze({
318    provider: requiredEnum('personalCloudProvider'),
319    stage: optionalEnum('personalCloudStage')
320  }),
321  personal_cloud_backup_succeeded: Object.freeze({
322    provider: requiredEnum('personalCloudProvider'),
323    stage: optionalEnum('personalCloudStage')
324  }),
325  personal_cloud_backup_failed: Object.freeze({
326    provider: requiredEnum('personalCloudProvider'),
327    errorCode: requiredEnum('personalCloudErrorCategory'),
328    stage: optionalEnum('personalCloudStage')
329  }),
330  personal_cloud_restore_started: Object.freeze({
331    provider: requiredEnum('personalCloudProvider'),
332    surface: requiredEnum('personalCloudSurface'),
333    stage: optionalEnum('personalCloudStage')
334  }),
335  personal_cloud_restore_succeeded: Object.freeze({
336    provider: requiredEnum('personalCloudProvider'),
337    stage: optionalEnum('personalCloudStage')
338  }),
339  personal_cloud_restore_failed: Object.freeze({
340    provider: requiredEnum('personalCloudProvider'),
341    errorCode: requiredEnum('personalCloudErrorCategory'),
342    stage: optionalEnum('personalCloudStage')
343  }),
344  personal_cloud_disconnected: Object.freeze({
345    provider: requiredEnum('personalCloudProvider'),
346    stage: optionalEnum('personalCloudStage')
347  }),
348  // Feature 82/88 account deletion and data lifecycle. Coarse outcome only — never account ID,
349  // name, email, or any Workspace/invoice content.
350  account_export_requested: Object.freeze({}),
351  account_deletion_completed: Object.freeze({
352    success: requiredBoolean
353  })
354});
355
356function assertPlainPayload(payload) {
357  if (payload === undefined) return {};
358  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
359    throw new TypeError('Product event properties must be a plain object.');
360  }
361  const prototype = Object.getPrototypeOf(payload);
362  if (prototype !== Object.prototype && prototype !== null) {
363    throw new TypeError('Product event properties must be a plain object.');
364  }
365  return payload;
366}
367
368function validateField(eventName, fieldName, value, rule) {
369  if (rule.type === 'boolean') {
370    if (typeof value !== 'boolean') {
371      throw new TypeError(`Product event "${eventName}" requires a boolean "${fieldName}" property.`);
372    }
373    return;
374  }
375
376  const allowedValues = ENUMS[rule.enumName];
377  if (!allowedValues?.includes(value)) {
378    throw new TypeError(`Product event "${eventName}" has an invalid "${fieldName}" property.`);
379  }
380}
381
382export function validateProductEvent(eventName, payload = {}) {
383  const definition = PRODUCT_EVENT_DEFINITIONS[eventName];
384  if (!definition) throw new TypeError(`Unknown product event "${String(eventName)}".`);
385
386  const properties = assertPlainPayload(payload);
387  const definedFields = new Set(Object.keys(definition));
388  const unknownFields = Object.keys(properties).filter((field) => !definedFields.has(field));
389  if (unknownFields.length) {
390    throw new TypeError(`Product event "${eventName}" contains unsupported properties: ${unknownFields.join(', ')}.`);
391  }
392
393  Object.entries(definition).forEach(([fieldName, rule]) => {
394    const hasValue = Object.hasOwn(properties, fieldName);
395    if (!hasValue) {
396      if (rule.required) throw new TypeError(`Product event "${eventName}" is missing "${fieldName}".`);
397      return;
398    }
399    validateField(eventName, fieldName, properties[fieldName], rule);
400  });
401
402  return Object.freeze({
403    name: eventName,
404    properties: Object.freeze({ ...properties })
405  });
406}
407
408export function createNoOpProductEventSink() {
409  return Object.freeze({
410    kind: 'noop',
411    record(eventName, payload = {}) {
412      validateProductEvent(eventName, payload);
413    }
414  });
415}
416
417export function createConsoleProductEventSink(logger = globalThis.console) {
418  if (!logger || typeof logger.info !== 'function') {
419    throw new TypeError('A console-style logger with an info method is required.');
420  }
421  return Object.freeze({
422    kind: 'console',
423    record(eventName, payload = {}) {
424      const event = validateProductEvent(eventName, payload);
425      logger.info('[InvoiceCraftly product event]', event.name, event.properties);
426    }
427  });
428}
429
430let activeProductEventSink = createNoOpProductEventSink();
431
432export function setProductEventSink(sink) {
433  if (!sink || typeof sink !== 'object' || typeof sink.record !== 'function') {
434    throw new TypeError('A product event sink with a record method is required.');
435  }
436  activeProductEventSink = sink;
437  return activeProductEventSink;
438}
439
440export function resetProductEventSink() {
441  activeProductEventSink = createNoOpProductEventSink();
442  return activeProductEventSink;
443}
444
445// The editor imports this stable delegating object. Production remains a validating
446// no-op until the separately approved first-party measurement module installs its
447// consent-gated sink. This keeps the event contract isolated from network code.
448export const productEvents = Object.freeze({
449  get kind() {
450    return activeProductEventSink.kind || 'custom';
451  },
452  record(eventName, payload = {}) {
453    return activeProductEventSink.record(eventName, payload);
454  }
455});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.