Skip to content

feat(data-warehouse): implement chatwoot import source - #71251

Merged
talyn-app[bot] merged 5 commits into
masterfrom
posthog-code/chatwoot-warehouse-source
Jul 17, 2026
Merged

feat(data-warehouse): implement chatwoot import source#71251
talyn-app[bot] merged 5 commits into
masterfrom
posthog-code/chatwoot-warehouse-source

Conversation

@Gilbert09

Copy link
Copy Markdown
Member

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:

  • 8 tables: conversations, messages, contacts, inboxes, agents, teams, labels, custom_attribute_definitions, declared in settings.py with per-endpoint response shape, params, primary keys, and stable created_at partition keys.
  • Base class: 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.
  • Pagination: page-number lists walk until an empty page (page sizes are fixed server-side and env-configurable on self-hosted installs, so an empty page is the only reliable end signal). Messages are fetched per conversation with Chatwoot's id-based after cursor (100/page, ascending), with per-conversation page caps and 404-skip for conversations deleted mid-sync. Conversations are requested with status=all (the API defaults to open-only) and sort_by=created_at_asc for page stability.
  • No incremental sync declared: Chatwoot's list endpoints have no server-side timestamp filter, so every schema is full refresh, per the "client-side cursors aren't incremental" rule.
  • Webhooks as the delta path: conversations and messages support 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's sha256= 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's signing_secret. A table transformer normalizes webhook rows to the REST shape (ISO created_at to epoch, string message_type to the integer enum, conversation_id from the nested conversation) and dedupes within a batch keeping the latest event per id.
  • Self-hosted support: configurable instance URL plus numeric account id. The host is customer-controlled, so the transport enforces HTTPS, blocks internal IPs at create and sync time, refuses redirects, caps response size/time, and declares host as a connection_host_fields entry 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.
  • Non-retryable error mapping for 401/403/404 and host/config errors, canonical_descriptions.py for all 8 tables, lists_tables_without_credentials=True so the public docs render the table catalog, and a SOURCES.md entry.

API behavior (response shapes, status default, sort enums, after cursor 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 (docsUrl points 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 dropping status=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.
  • Ran the full source-registry suites (sources/tests/, 1634 passed) plus the 79 new chatwoot tests; ruff check/format clean; hogli ci:preflight --fix reports no failures; audit_source_docs passes for the new doc against a posthog.com checkout.

Note: regenerating generated_configs.py wholesale 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

  • Publish to changelog?
  • Alert Sales and Marketing teams?

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, Webhook model, lib/webhooks/trigger.rb) and its swagger spec; the code was treated as authoritative over the docs. Notable decisions: messages use the after cursor (100/page, ascending) instead of the documented before walk (20/page, descending); contacts stay pull-only because contact webhook payloads carry no timestamps and would null created_at on 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

@github-actions

Copy link
Copy Markdown
Contributor

Hey @Gilbert09! 👋

It looks like your git author email on this PR isn't your @posthog.com address (owerstom@gmail.com). Since you're on the PostHog team, it's worth pointing your local git author email at your @posthog.com address. Why it matters:

  • Consistent work identity in git history — internal tooling that attributes commits to team members keys off your @posthog.com address.
  • Keeps team contributions easy to tell apart from external community ones when scanning history.

You can fix it for this repo with:

git config user.email "you@posthog.com"

Or set it globally with git config --global user.email "you@posthog.com". No need to redo this PR — just a nudge for next time. 🙂

@veria-ai

veria-ai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 3 · PR risk: 0/10

@trunk-io

trunk-io Bot commented Jul 15, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

⚠️ Backend coverage — 89.0% of changed backend lines covered — 89 uncovered

🧪 Backend test coverage

Patch coverage — changed backend lines (products + core): ██████████████████░░ 89.0% (721 / 810)

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.

@Gilbert09
Gilbert09 force-pushed the posthog-code/chatwoot-warehouse-source branch from bc64fd0 to 04481e8 Compare July 15, 2026 21:43
@hex-security-app

Copy link
Copy Markdown

Verified. account_id is now included in connection_host_fields, and the update serializer rejects a connection-target change while a sensitive credential is preserved, so an account-only update requires re-entering the token.

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
@Gilbert09
Gilbert09 force-pushed the posthog-code/chatwoot-warehouse-source branch from 3d4bc23 to ac38ad4 Compare July 17, 2026 11:41
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
@talyn-app
talyn-app Bot enabled auto-merge (squash) July 17, 2026 12:24
@talyn-app
talyn-app Bot merged commit e7fd18e into master Jul 17, 2026
240 checks passed
@talyn-app
talyn-app Bot deleted the posthog-code/chatwoot-warehouse-source branch July 17, 2026 12:28
@deployment-status-posthog

deployment-status-posthog Bot commented Jul 17, 2026

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-07-17 13:13 UTC Run
prod-us ✅ Deployed 2026-07-17 13:29 UTC Run
prod-eu ✅ Deployed 2026-07-17 13:33 UTC Run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants