Accounting Import Events (Amplitude) #
The AI accounting import (apps/webapp/src/features/entity-setting/components/accounting/v2)
emits a snake_case event set to the existing Amplitude project. Extend this catalogue rather than
inventing parallel names.
How it is wired #
- Names and the per-event property allow-list:
…/accounting/v2/constants/tracking-events.ts. - Emitter:
…/accounting/v2/tracking/use-import-tracking.ts, which wraps the houseuseTracking()hook (shared/components/monitor/hooks/use-tracking.tsx) so every event stays behind Cookiebot consent. Never import@amplitude/analytics-browserin the feature — a test fails the build if you do. sanitizeProps(…/tracking/payload.ts) drops any property the event does not allow-list, caps arrays at 20 items and strings at 200 characters, and buckets file sizes. The allow-list is the privacy boundary, enforced at runtime rather than by review.
Shared properties #
Stamped onto every event: import_flow_id, entity_id, organization_id, user_id,
source_software, source_is_other, has_integration, mode (ai_import | prefill),
model_tier, flow_variant, plus category: accounting_import.
import_flow_id is minted when an upload settles (or when the prefill screen opens) and is the join
key for the whole funnel — a re-upload starts a new id.
Events #
Funnel #
| Event | Fires when | Notable properties |
|---|---|---|
upload_screen_viewed |
Upload screen opens | has_prefill_data |
source_software_selected |
Software picked | changed_from_default |
upload_onboarding_started |
Upload settles, AI runs fire | file_count, file_extension(s), estimated_rows, size_bucket, has_csv, has_excel, is_reupload |
upload_file_rejected |
File fails validation | error_code, file_extension, size_bucket |
upload_file_removed |
File removed from the pool | file_extension, remaining_count |
prefill_mode_shown |
Connected-integration variant | provider, has_prefill_data |
manual_entry_chosen |
Falls back to the V1 form | stage, duration_seconds |
onboarding_flow_completed |
Save succeeds | duration_seconds, total_records, records_<table>, correction_count_total, retry_count, failed_run_count, recovery_used, steps_revisited |
onboarding_flow_abandoned |
Back / manual entry / discarding re-upload / navigate away / unmount | reason, last_step_reached, last_step_table, tables_accepted, unsaved_records |
AI quality #
| Event | Fires when | Notable properties |
|---|---|---|
ai_run_started |
A category’s run acquires its gate slot | gate_wait_ms, retry_token, pool_version |
ai_run_completed |
Model result validated | duration_ms, field_count, steps_streamed |
ai_run_failed |
Run settles with no result | duration_ms, error_kind |
ai_run_retried |
Retry pressed | retry_index, was_encode_retry |
import_encode_failed |
Pool encode throws (corrupt/protected workbook) | file_count, has_excel |
ai_mapping_suggested |
Suggestion becomes visible (single commit point) | confidence_score, confidence_min, low_confidence_count, assumed_count, filled_count, field_count, row_count, needs_normalization, duration_ms |
ai_mapping_corrected |
Per corrected column, at step accept | field_name, correction_count, rows_changed, correction_kind, was_assumed, field_confidence, and for controlled-vocabulary columns only original_suggestion / final_selection |
ai_normalization_started / _completed |
Normalisation pass | row_count, mapped_field_count, outcome (applied | schema_invalid | settled_without_result) |
ai_missing_fields_reported |
Run reports missing fields | missing_field_count, missing_fields (tofu field keys) |
Review behaviour #
| Event | Fires when | Notable properties |
|---|---|---|
review_step_viewed / _accepted / _revisited |
Wizard navigation | step_index, time_on_step_ms, edited_before_accept, correction_count, corrections_unavailable |
validation_error_shown |
Active step invalid (deduped per failure) | missing_fields, invalid_fields, error_row_count |
sheet_first_edited |
First divergence from the AI’s sheet | ms_since_suggestion |
expanded_ai_suggestion |
Chain-of-thought section opened | step_category, step_count, is_live_turn |
recovery_guide_requested / _shown |
Missing-field guide | missing_field_count, source_count, has_video, duration_ms |
recovery_source_clicked |
Guide link clicked | host (hostname only), is_video |
save_failed |
Save rejected | error_kind, duplicate_field, records_attempted |
Deliberately not tracked #
Cell values, raw file names, contact details (names, emails, tax IDs), the AI summary and
chain-of-thought text, and extractedData rows never reach Amplitude. The one exception is
ai_mapping_corrected’s original_suggestion / final_selection, which are only attached for
columns whose values come from lists we own (account type, currency, tax rate, tag parent, default
account) — see CONTROLLED_FIELDS in …/tracking/corrections.ts.
How corrections are derived #
There is no explicit mapping UI: the AI writes into the review sheets and the user edits cells. At
each step accept, the live sheet is diffed against the snapshot taken when the AI’s sheet was merged.
Rows are paired on each table’s identity column so sorting is not mistaken for mass corrections, and
if more than 75% of rows changed (on a sheet of at least four rows) the diff reports
corrections_unavailable instead of noise.
The accepted-import record #
On a successful save the client also POSTs a diagnostic record to
/api/accounting-import/log, which validates it and writes one structured log line
(label: accounting_import_record) to Datadog. It carries file metadata, per-table mapping details
(tofu field, source header, confidence, assumed/filled flags), validation results and correction
counts — no cell values. Retention is Datadog log retention; if this record ever needs to be queried
months later it should move to a bonsai table.
Reading the data #
- Events are gated on Cookiebot statistics/marketing consent, so every count is a sample, not a census. Note it on any dashboard.
- Nothing is emitted in local dev (
shouldMonitorEnvironment); the hook logsTracking action: <name>at debug level instead, which is how you verify wiring locally. duration_secondsstarts when the upload settles, not when the file picker opens.
Session replay #
Replay runs at sampleRate: 1. consent-manager.ts blocks .bonsai-spreadsheet (the grid canvas)
and masks .bonsai-import-conversation plus any [data-amp-mask] element (the uploaded file list),
so recordings show how the UI behaved without shipping the customer’s books.