feat(data-warehouse): implement chatwoot import source - #71251
Conversation
|
Hey @Gilbert09! 👋 It looks like your git author email on this PR isn't your
You can fix it for this repo with: git config user.email "you@posthog.com"Or set it globally with |
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 3 · PR risk: 0/10 |
🤖 CI report
|
| File | Patch | Uncovered changed lines |
|---|---|---|
products/warehouse_sources/backend/temporal/data_imports/sources/chatwoot/chatwoot.py |
79.5% | 158, 181, 185, 188, 207–208, 211–212, 227–228, 234, 237, 239, 255, 269, 281, 313, 360, 366, 375, 385, 436, 445–447, 455–457, 505, 508, 524–525, 535–536, 544, 596, 598, 602, 604–606, 618–621, 623–625, 627–628, 630–631, 638–646, 657–660, 662–665, 671–672, 693–703, 728 |
products/warehouse_sources/backend/temporal/data_imports/sources/chatwoot/source.py |
89.1% | 153, 157, 243–244, 251, 256 |
🤖 Agents: add a test covering the lines above, or note why under "How did you test this code?". Machine-readable gap list: the patch-coverage artifact on this run (gh run download 29578675102 -n patch-coverage), or the coverage-data block at the end of this comment.
Per-product line coverage (touched products)
| Product | Coverage | Lines |
|---|---|---|
demo |
███████████░░░░░░░░░ 56.2% |
1,497 / 2,663 |
tasks |
█████████████░░░░░░░ 67.2% |
25,898 / 38,558 |
signals |
████████████████░░░░ 79.1% |
19,074 / 24,116 |
data_modeling |
████████████████░░░░ 80.0% |
4,834 / 6,045 |
cdp |
████████████████░░░░ 80.7% |
3,118 / 3,864 |
notebooks |
█████████████████░░░ 84.8% |
6,903 / 8,141 |
cohorts |
█████████████████░░░ 86.2% |
4,065 / 4,717 |
agent_platform |
█████████████████░░░ 86.4% |
3,807 / 4,405 |
actions |
█████████████████░░░ 86.6% |
717 / 828 |
product_tours |
█████████████████░░░ 87.5% |
1,266 / 1,447 |
exports |
██████████████████░░ 88.4% |
6,904 / 7,813 |
conversations |
██████████████████░░ 89.0% |
16,183 / 18,186 |
mcp_analytics |
██████████████████░░ 89.2% |
2,514 / 2,819 |
dashboards |
██████████████████░░ 89.3% |
5,841 / 6,540 |
engineering_analytics |
██████████████████░░ 89.3% |
5,407 / 6,053 |
error_tracking |
██████████████████░░ 89.6% |
9,731 / 10,862 |
alerts |
██████████████████░░ 90.0% |
3,655 / 4,063 |
streamlit_apps |
██████████████████░░ 90.4% |
2,499 / 2,764 |
stamphog |
██████████████████░░ 90.4% |
3,504 / 3,875 |
slack_app |
██████████████████░░ 90.6% |
9,018 / 9,955 |
marketing_analytics |
██████████████████░░ 90.8% |
11,514 / 12,684 |
product_analytics |
██████████████████░░ 91.3% |
5,757 / 6,304 |
data_warehouse |
██████████████████░░ 92.3% |
18,345 / 19,875 |
web_analytics |
███████████████████░ 92.7% |
13,535 / 14,597 |
workflows |
███████████████████░ 92.8% |
5,482 / 5,909 |
ai_observability |
███████████████████░ 92.8% |
14,916 / 16,077 |
surveys |
███████████████████░ 92.9% |
5,689 / 6,122 |
posthog_ai |
███████████████████░ 93.2% |
1,322 / 1,418 |
approvals |
███████████████████░ 93.3% |
3,395 / 3,640 |
reminders |
███████████████████░ 93.4% |
468 / 501 |
early_access_features |
███████████████████░ 93.8% |
848 / 904 |
endpoints |
███████████████████░ 94.1% |
8,606 / 9,143 |
revenue_analytics |
███████████████████░ 94.5% |
3,598 / 3,809 |
skills |
███████████████████░ 94.5% |
2,881 / 3,049 |
review_hog |
███████████████████░ 94.6% |
6,789 / 7,174 |
logs |
███████████████████░ 95.3% |
9,572 / 10,044 |
experiments |
███████████████████░ 95.7% |
24,417 / 25,527 |
replay_vision |
███████████████████░ 95.8% |
13,776 / 14,383 |
annotations |
███████████████████░ 96.2% |
732 / 761 |
feature_flags |
███████████████████░ 96.3% |
16,172 / 16,794 |
warehouse_sources |
███████████████████░ 96.4% |
272,769 / 282,911 |
user_interviews |
███████████████████░ 96.4% |
2,242 / 2,325 |
data_catalog |
███████████████████░ 97.2% |
2,117 / 2,179 |
customer_analytics |
███████████████████░ 97.2% |
7,480 / 7,698 |
pulse |
████████████████████ 98.4% |
2,012 / 2,044 |
Report-only. Patch coverage = changed backend lines covered vs origin/master. Sorted lowest first.
Known gaps: lines covered only by Temporal tests show as uncovered; core line numbers may drift if master changed the same file.
bc64fd0 to
04481e8
Compare
|
Verified. |
Implements the Chatwoot warehouse source: eight tables (conversations, messages, contacts, inboxes, agents, teams, labels, custom attribute definitions) pulled from the Chatwoot Application API, with webhook-based real-time deltas for conversations and messages, resumable full refreshes, and support for both Chatwoot Cloud and self-hosted instances. Generated-By: PostHog Code Task-Id: 66125383-97cb-48a1-a7d0-64b2b0e33015
The webhook create/update/delete calls buffered response bodies with requests' default stream=False, so a customer-controlled Chatwoot host could exhaust an API worker's memory during automatic webhook setup. Route them through the same redirect-refusing, size- and time-capped body reader the pull path already uses. Generated-By: PostHog Code Task-Id: 66125383-97cb-48a1-a7d0-64b2b0e33015
…pagination Treat account_id as a connection target alongside host, so changing the selected Chatwoot account forces re-entering the API access token instead of reusing the preserved one — a single user token can belong to several accounts, and an editor who cannot read the stored token could otherwise retarget the import at another account's data. Also add an aggregate message-page cap across the whole message walk. The per-conversation cap alone still let a hostile self-hosted host multiply requests without bound (conversations × pages); the aggregate ceiling bounds the total message pages one sync fetches. Generated-By: PostHog Code Task-Id: 23909f82-63c1-44a2-af38-64b1aa0348d4
3d4bc23 to
ac38ad4
Compare
oxfmt normalizes the emoji column display width; align the chatwoot row's trailing padding so the markdown format check passes. Generated-By: PostHog Code Task-Id: fbebd797-1d5e-49b2-9125-6311ab405962
urlparse and the HTTP client can disagree on where the host ends when the URL contains a userinfo separator or a (raw or percent-encoded) backslash, so a host like `https://169.254.169.254\@attacker.example` passed the allowlist as `attacker.example` while requests could connect to the internal IP — an SSRF bypass. Reject these characters outright before validation, matching the unstructured source. Generated-By: PostHog Code Task-Id: fbebd797-1d5e-49b2-9125-6311ab405962
Problem
Chatwoot was a scaffolded warehouse source stub (registered but hidden behind
unreleasedSource=True, no sync logic). Teams using Chatwoot for customer support couldn't pull their conversations, messages, or contacts into the PostHog data warehouse.Changes
Implements the Chatwoot source end-to-end and ships it visible with
releaseStatus=ALPHA:conversations,messages,contacts,inboxes,agents,teams,labels,custom_attribute_definitions, declared insettings.pywith per-endpoint response shape, params, primary keys, and stablecreated_atpartition keys.ResumableSource + WebhookSource. Page-number endpoints resume from a saved page; the messages fan-out resumes from a (conversation, cursor) bookmark. State is saved after each yielded batch.aftercursor (100/page, ascending), with per-conversation page caps and 404-skip for conversations deleted mid-sync. Conversations are requested withstatus=all(the API defaults to open-only) andsort_by=created_at_ascfor page stability.conversationsandmessagessupport webhook sync. Webhook create/reconcile/info/delete are managed via Chatwoot's webhooks API (admin token required; 422-on-duplicate reconciles the existing hook since URLs are unique per account). The Hog template verifies Chatwoot'ssha256=HMAC signature (X-Chatwoot-Timestamp.body), with a bypass toggle for older self-hosted versions that don't sign. The per-webhook secret returned at creation is auto-filled into the function'ssigning_secret. A table transformer normalizes webhook rows to the REST shape (ISOcreated_atto epoch, stringmessage_typeto the integer enum,conversation_idfrom the nested conversation) and dedupes within a batch keeping the latest event per id.hostas aconnection_host_fieldsentry so retargeting it re-requires the token. Resume state stores bare page numbers/ids, never URLs, so stale state can't replay against an edited host.canonical_descriptions.pyfor all 8 tables,lists_tables_without_credentials=Trueso the public docs render the table catalog, and a SOURCES.md entry.API behavior (response shapes,
statusdefault,sortenums,aftercursor semantics, webhook payloads and signing, id semantics) was verified against the Chatwoot Rails source and in-repo swagger spec rather than a live account - see agent context.Docs PR: PostHog/posthog.com#18560 (
docsUrlpoints at/docs/cdp/sources/chatwoot).How did you test this code?
Automated tests only - I don't have a live Chatwoot account, so no manual end-to-end sync was performed (hence ALPHA).
tests/test_chatwoot.py(transport): pagination walks pages until empty and would catch droppingstatus=all(which would silently sync only open conversations); resume-from-saved-state for both page-number and messages-bookmark paths; save-state-after-yield ordering; messages fan-out cursor advancement, short-page termination, and deleted-conversation 404 skip; webhook row normalization (type coercions that would otherwise break delta merges) and in-batch dedupe; webhook create/permission/422-reconcile/delete flows; HTTPS + account-id injection guards; credential validation status mapping.tests/test_chatwoot_source.py(source class): schema catalog (nothing advertises incremental, webhook flags on exactly conversations/messages), non-retryable error matching (and non-matching for transient 5xx),connection_host_fields, resumable manager binding, webhook template/resource map, and argument plumbing.sources/tests/, 1634 passed) plus the 79 new chatwoot tests;ruff check/formatclean;hogli ci:preflight --fixreports no failures;audit_source_docspasses for the new doc against a posthog.com checkout.Note: regenerating
generated_configs.pywholesale produced unrelated diffs to other sources (e.g. Stripe auth fields), i.e. the committed file has drifted from current generator output, so I applied only the Chatwoot block (identical to the generator's output for it).Automatic notifications
Docs update
Doc added in PostHog/posthog.com#18560.
🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Authored by Claude (PostHog Code session). Skills invoked:
implementing-warehouse-sources,documenting-warehouse-sources,writing-tests. Since no live Chatwoot credentials were available for curl verification, a research pass verified every endpoint's behavior against the Chatwoot Rails codebase (controllers, jbuilder views,MessageFinder,Webhookmodel,lib/webhooks/trigger.rb) and its swagger spec; the code was treated as authoritative over the docs. Notable decisions: messages use theaftercursor (100/page, ascending) instead of the documentedbeforewalk (20/page, descending); contacts stay pull-only because contact webhook payloads carry no timestamps and would nullcreated_aton merge; all schemas are full refresh because no list endpoint filters server-side on time (webhooks cover deltas). Deliveries are HMAC-verified by default with an explicit bypass for pre-signing self-hosted versions - unverified against a live install, flagged as the main ALPHA uncertainty.Created with PostHog Code