mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
716 files rewritten, 30 renamed, same family mapping as the frontend pass.
Held back, and each for a reason rather than by oversight:
db_table values insights_hogfunction, insights_hogflow,
insights_hogfunctiontemplate, hogflow_templates.
Renaming a table is ALTER TABLE, so it ships as a
migration with the raw SQL in nodejs that names it.
hog_invocation_results a Datastore table with Kafka engine tables on it
products/review_hog the module path is the Django app path; moving it
moves app_label and every migration is recorded under
(app_label, name). That is the same history break as
renaming a migration file, one level up.
*/migrations/* five files, untouched
whogho an integration vendor
Checked: 395 of 397 changed Python files compile. The two that do not —
insightsql/database/database.py and temporal/schedule.py — fail identically at
origin/main under the same interpreter, because they use `type X = ...` and
`except*`, which need the 3.13 this repo pins and not the 3.9 doing the check.
Running the check against the original is what told those apart from a real
break.
Co-authored-by: Hanzo Dev <[email protected]>
11 KiB
11 KiB
Customer analytics — v1 compromises
Shortcuts taken to ship the first version. Revisit when they bite.
Select custom property option side effects
- Rename backfill / removal clearing runs inline.
update_custom_property_definitionrewrites the definition'sCustomPropertyValuerows synchronously, inside the definition-save transaction (apply_option_side_effectsinlogic/custom_property_definitions.py). For a definition with many account values this makes the PATCH slow and holds the transaction open. Brittle at scale — move to an async backfill job when large value sets bite. - Conversion safety lives in a caller-side guard. Side effects run only while the definition is still
select: on a select→other conversion the id diff would read every option as removed and wrongly clear all values, which must instead survive as plain strings. The guard sits in the facade update path, locked bytest_converting_select_to_text_keeps_values_and_clears_options; a new write path that skips it would clear values on conversion.
Custom property view sync
- Celery, not Temporal. The sync runs in a Celery task, not a dedicated Temporal workflow. No
durable retries and no run-level UI inspection — failures surface via
last_sync_erroron the source + error tracking. Move to Temporal if we need durable retries, long-running syncs, or per-run inspection. - No retries. A failed run is not retried; it records the failure (advancing the auto-disable streak) and waits for the next materialization. Only transient write conflicts retry, in-logic.
- Direct dispatch by task name, not a signal. Core (
succeed_materialization_activity) enqueues this product's sync task by name viasend_task. A generic core signal the product subscribes to would keep core ignorant of the consumer, but for a single consumer that isn't worth the wiring (a signal definition + a receiver + an app-ready hook). By-name dispatch also keeps the product and InsightsQL out of the data-modeling worker's process, since core imports nothing from here. Trade-off: core hardcodes this consumer's task name. If a second consumer ever needs the materialization event, switch to a core-owned signal (inversion of control) rather than adding another direct dispatch. - No save-time column validation. Creating/updating a source does not check that
source_column/key_columnexist in the view's schema. A bad column surfaces as a per-source sync error (and advances the auto-disable streak) on the next run, not as a 400 on save. Validate against the saved query'scolumnsat write time if the delayed feedback bites. - Initial sync is best-effort. Saving an enabled source enqueues a sync on commit so values populate without waiting for the next materialization. If the broker is down the save still succeeds and the enqueue is dropped (logged to error tracking) — the next materialization recovers.
- Create-path sync is synchronous, best-effort, and workflow-only. When the external create
endpoint is called by a workflow "Create account" step (the
X-Insights-Script-Flow-Idheader), it syncs warehouse-backed custom properties for the new account inline — scoped to its external id, materialized views only — so the next workflow step can read the values. Creates without the header skip the sync, which keeps the per-request warehouse fan-out off the general create path; the header is caller-supplied, so a token holder can opt in by faking it — same trust level as the existing workflow attribution. Failures are captured and swallowed: creation never fails, and no sync outcome is recorded (streaks andlast_synced_atbelong to the scheduled full sync). Values are as fresh as the last materialization. All enabled sources sync, not just properties the workflow references — read-side usage isn't indexed, and one filtered query per view is cheap. If the added request latency bites, narrow to workflow-referenced properties or make the step poll. - v2 materialization only. v1
run_workflow.pyis frozen and does not dispatch the sync; v1 teams get it after migrating to v2.
Account relationships — leftover JSON role keys in stored rows
The relationship tables are the only source of truth for account roles; the JSON role keys
(csm, account_executive, account_owner) are gone from AccountProperties.
Stored Account._properties rows may still carry them until
manage.py backfill_account_relationships has run in the environment
(it assigns any not-yet-migrated JSON holders as relationships, then strips the keys).
Until then Account.properties silently ignores unknown stored keys, and the raw keys remain
visible through the InsightsQL system.accounts.properties JSON column.
Delete this section (and the getter's key filter can revert to a plain model_validate)
once the command has run everywhere.
External account custom_properties payload
- Every definition is emitted, unbounded.
_to_external_accountincludes every team custom property definition keyed by name (nullwhen unset) so workflow result paths are deterministic. The flow executor caps all workflow variables at 5 KB of JSON combined and the Get account node's defaultaccountvariable stores the whole response body, so a team with enough definitions (roughly 100–150 at typical name lengths, fewer with long names or populated values) makes every Get account step throw and drop its variables — even in workflows that never touch custom properties. If this bites: stop emitting unset definitions asnull, or drop the whole-body default variable in favor of path-scoped ones. - Output variable suggestions read one page. The workflow editor's suggestion chips fetch
custom property and relationship definitions without pagination (default page size 100), so
definitions past the first page silently never appear as suggestions — the value still exists
in the payload and can be mapped by hand. Follow
nextpages ingetOutputMappingSuggestions(workflows frontend registry) if teams that large show up.
Event stream
- Cascade deletions don't archive the Slack destination. The managed InsightsFunction is archived
in
delete_event_stream(the facade, the only deliberate deletion path) and indelete_event_streams_for_user(called from core'sOrganizationMembershippost_delete, so a member leaving or being removed from the org stops their stream delivering) rather than apre_deletesignal on the stream itself, so plain ORM cascades still bypass it. Team deletion is harmless — the InsightsFunction is team-scoped and dies in the same cascade. Hard-deleting an owner (created_byCASCADE) usually archives via the membership post_delete firing in the same collector run, but the ordering isn't guaranteed. Accepted because user hard-deletion is rare (members are deactivated or removed from the org, not deleted); if it bites, archive the destinations explicitly in the user-deletion flow rather than reintroducing a signal.
Account custom property changed workflow trigger
- Volatile synced columns can flood workflows. A view column that genuinely changes for many
accounts each sync emits one
$account_custom_property_changedevent — and starts one workflow run — per account per sync run. No cap and no emission-volume observability. Acceptable while the product is internal-only; before external exposure add emission counts toSyncResultand guidance to trigger on stable columns. - Multi-property triggers fan out. One event per changed property, so one run per property, never one per batch write. The trigger UI warns; routing logic is the workflow author's job.
- Cross-workflow loops are damped, not prevented. Same-value writes never emit and frequency masking caps rate, but value-flapping loops (workflow A sets X→1, triggering B which sets X→2, …) depend on runtime values and cannot be detected statically. The save-time cycle advisory is best-effort — static tag/property references only, never a save blocker.
- Frequency options are account-keyed because account events carry no person. The generic event
trigger's frequency options hash on
{person.id}, which resolves empty on person-less events and would mask globally across accounts. CA triggers ship account-keyed options; the generic event trigger keeps its person-keyed options and retains this gap for person-less events. - current/previous are event properties, not workflow variables. Same
{event.properties.*}templating access everywhere; auto-populated variables would need executor and trigger-schema changes. Revisit only if variable semantics (mutation, Variables taxonomic category) are needed. The values land as analytics events in the team's own project (the established pattern — conversation events carry old/new status and truncated message content the same way), so they are visible to anyone with project access, like the rest of the account's data. - No unset/delete emission. No value-delete path exists; if one is added, its author decides whether removal counts as a change.
Account channel summaries
- The channel binding is trusted as written.
slack_channel_idis an account property any account editor can set, and the summary pipeline reads whatever channel it names with the team's own Support bot token. An editor can therefore point an account at any channel that team's bot is in — including a private channel the editor isn't a member of — and read its summary. Accepted for now: editors are internal team members, the token is team-scoped (no cross-team reach), and the announcements feature already posts through the same binding. The activity re-resolves the binding, cadence, and org AI-processing approval from the DB just before fetching, so stale or forged workflow inputs can't widen this. If summaries ever cover channels whose membership matters, validate the binding at write time against a server-side channel policy instead.
Slack workspace URL is hardcoded
SLACK_ARCHIVES_ORIGINhardcodes Insights's own workspace — inbackend/constants.pyand mirrored infrontend/components/Accounts/accountLinksLogic.ts. Every Slack link built from it (Useful links sidebar, Slack summary message permalinks, channel summary citations) is wrong for any team other than us. Fine while the product is Insights-internal; must be fixed before GA. The correct value is per-team and owned by conversations: the bot'sauth.testresponse carries the workspaceurl(same callget_bot_user_id_cachedalready caches a field from), so the fix is a cached lookup in conversations exposed through its facade, consumed here and by the frontend.
Tech debt
- Account property writes have no single choke point.
Account._propertiesis mutated from independent paths:create_account_for_view/update_account_for_view(via the manager) and the Max tool's_create_account/_update_account. Anything that must happen on every properties write has to be repeated per call site, and a new writer can silently forget it. Funneling every properties write throughAccountManager(and hooking cross-cutting behavior there) is the fix if account properties grow more derived behavior.