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.