Skip to content

feat(data-warehouse): implement heroku import source#70713

Merged
talyn-app[bot] merged 4 commits into
masterfrom
posthog-code/heroku-warehouse-source
Jul 15, 2026
Merged

feat(data-warehouse): implement heroku import source#70713
talyn-app[bot] merged 4 commits into
masterfrom
posthog-code/heroku-warehouse-source

Conversation

@Gilbert09

@Gilbert09 Gilbert09 commented Jul 14, 2026

Copy link
Copy Markdown
Member

Problem

Heroku was a scaffolded warehouse source with no sync logic. Heroku account data (deploy history, dyno formations, add-on usage, invoices) is useful warehouse material for engineering and finance analytics, and the Platform API v3 is stable and self-serve (any user can mint a long-lived API key).

Changes

Implements the Heroku source end to end, following the standard source.py / settings.py / heroku.py split:

  • 11 tables: apps, addons, builds, collaborators, domains, dynos, formation, invoices, pipelines, releases, teams. Endpoint catalog is declarative in settings.py.
  • Auth: API key as a Bearer token, validated with a cheap GET /account probe. 401/403 are registered as non-retryable errors.
  • Pagination: Heroku pages via HTTP headers (a Range request header and a Next-Range response cursor), so the transport uses a small custom paginator instead of a query-param cursor. Requests pin Accept: application/vnd.heroku+json; version=3 and sort ascending by id with max=1000.
  • Resumable imports (ResumableSource): the verbatim Next-Range cursor plus, for fan-out endpoints, a stable app-ID bookmark are persisted after each yielded page, so Temporal retries resume where they left off instead of restarting.
  • Per-app fan-out: builds, collaborators, domains, dynos, formation, and releases iterate the apps list and query each app's child endpoint. Heroku ids are globally unique UUIDs, so id stays a valid table-wide primary key. Apps deleted mid-sync (404) are skipped, and a per-list page cap guards against unbounded scans.
  • Full refresh only: the API exposes no updated-since/created-since filters, so no table advertises incremental sync (see "conservative choices" below).
  • Rate limiting: Heroku's token bucket (4,500 requests/hour) is handled with bounded exponential backoff on 429/5xx via tenacity. All outbound HTTP goes through make_tracked_session().
  • Docs plumbing: canonical table/column descriptions sourced from the official API reference, lists_tables_without_credentials = True so the posthog.com doc renders the table catalog, and a Heroku SVG icon.
  • SOURCES.md updated (heroku moved from Scaffolded to Implemented).

The source stays behind unreleasedSource=True with releaseStatus=alpha for now.

Note

Conservative choices made without live credentials. Endpoint shapes, Range attributes, and the 401 error format were verified against the live API and its published JSON hyper-schema, but paginated Next-Range flows need an authenticated account. Two things follow from that: pagination handling follows the documented contract (cursor resent verbatim, termination when Next-Range is absent) but hasn't run against a real multi-page account, and incremental sync was left off everywhere even though /apps advertises a Range on updated_at, since we couldn't smoke-test whether datetime ranges actually filter. Both are noted in code comments and are cheap follow-ups once someone runs a sync with a real account.

The user-facing doc is written and consistent (audit_source_docs shows no heroku findings); it lands separately in the posthog.com repo: PostHog/posthog.com PR to follow (linked in comments).

How did you test this code?

  • hogli test products/warehouse_sources/backend/temporal/data_imports/sources/heroku/tests – 37 tests pass.
    • Transport tests catch: cursor not resent verbatim / wrong initial Range header (page-boundary skips), resume state saved before yield instead of after (data loss on crash), a deleted app failing the whole sync instead of being skipped, credential errors being retried forever, transient 429/5xx not being retried, and unbounded pagination when the cap regresses.
    • Source-class tests catch: an endpoint flipping to supports_incremental without a server-side filter, get_non_retryable_errors keys drifting from the error strings requests actually produces (verified against the live API), canonical descriptions drifting from the endpoint catalog, and source_for_pipeline plumbing regressions.
  • python manage.py generate_source_configs regenerated HerokuSourceConfig; registry-wide suites (sources/tests/, 1551 tests) pass.
  • ruff check / ruff format clean; hogli ci:preflight --fix reports 0 failures.
  • Verified against the live API (unauthenticated): the JSON hyper-schema for endpoint paths, properties, and Range attributes, and the exact 401 error string over HTTP/1.1.
  • Not done: an authenticated end-to-end sync (no Heroku credentials available in the session).

Automatic notifications

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

Docs update

Doc added to posthog.com (contents/docs/cdp/sources/heroku.md) in a companion PR, following the documenting-warehouse-sources template. docsUrl matches the heroku slug.

🤖 Agent context

Autonomy: Fully autonomous

  • Built with Claude Code (PostHog Code cloud task). Skills invoked: implementing-warehouse-sources, documenting-warehouse-sources, writing-tests.
  • Decisions: shipped as ResumableSource pull-only rather than adding WebhookSource – Heroku webhooks are per-app (max 10 subscriptions each), so webhook management would need fan-out creation across every app plus drift handling for new apps, which isn't verifiable without credentials; left as a follow-up. Full refresh everywhere since the API has no server-side timestamp filters (Range-on-updated_at exists for /apps but couldn't be verified). generated_configs.py was regenerated then trimmed to the heroku hunk only, because the local environment lacks Stripe OAuth settings and the generator emitted an unrelated, environment-dependent Stripe diff.

Created with PostHog Code

@assign-reviewers-posthog
assign-reviewers-posthog Bot requested a review from a team July 14, 2026 13:05
@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

🦔 Hogbox preview · ✅ ready

▶ Open the preview

🔑 Login test@posthog.com / 12345678 (demo data)
🧩 Running this PR's backend and frontend, on the PostHog :master base
🔗 Link stable across rebuilds — a re-push swaps the box underneath, the URL stays
🔒 Access tailnet only (PostHog VPN)
🛠️ Admin inspect & debug state in hogland
💤 Idle sleeps after ~30 min idle (snapshot to S3, zero node cost) and wakes on your next visit in ~30s, behind a brief "waking up" screen

commit 6580a5b · box box-06b0880a86b7 · ready in 903s (push → usable) · build log · rebuilds on every push, torn down on close

@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. 🙂

Copy link
Copy Markdown
Member Author

Companion doc PR: PostHog/posthog.com#18461

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

Bundle size — no change

Uncompressed size of every built .js bundle, compared against the base branch.

Total: 64.77 MiB · no change

No file changed by more than 1000 B.

Posted automatically by build-bundle-size-report · uncompressed bytes from dist-report

Eager graph — within budget

How much code each root ships on the eager path — downloaded and parsed before the surface is interactive. Measured from the esbuild output chunks (post-tree-shake, static imports only); lazy import() / React.lazy chunks are not counted.

Root Eager (shipped) Δ vs base Budget
entry (logged-out pages, app bootstrap)
src/index.tsx
1.22 MiB · 22 files no change ███░░░░░░░ 28.4% of 4.29 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
8.13 MiB · 2,978 files no change █████████░ 87.9% of 9.25 MiB

🟢 node_modules/monaco-editor/ stays out of src/index.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 node_modules/monaco-editor/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx

Largest files eagerly shipped from src/index.tsx
Size File
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
24.6 KiB ../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.js
6.3 KiB ../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
4.5 KiB ../node_modules/.pnpm/@jspm+core@2.1.0/node_modules/@jspm/core/nodelibs/browser/process.js
3.9 KiB ../node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/cjs/scheduler.production.min.js
1.4 KiB ../node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js
1.3 KiB src/RootErrorBoundary.tsx
912 B ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js
789 B src/scenes/ChunkLoadErrorBoundary.tsx
762 B src/index.tsx
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
Size File
281.3 KiB ../node_modules/.pnpm/posthog-js@1.402.3/node_modules/posthog-js/dist/rrweb.js
267.7 KiB ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
235.5 KiB src/taxonomy/core-filter-definitions-by-group.json
222.9 KiB ../node_modules/.pnpm/posthog-js@1.402.3/node_modules/posthog-js/dist/module.js
164.0 KiB src/queries/validators.js
154.3 KiB ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
105.8 KiB src/lib/api.ts
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
92.7 KiB ../packages/quill/packages/quill/dist/index.js

Posted automatically by check-eager-graph · sizes are eager output bytes (shipped, post-tree-shake) from the esbuild metafile · part of #32479

Dist folder size — 🔺 +690 B (+0.0%)

Total size of the built frontend/dist folder (all assets), compared against the base branch.

Total: 1312.48 MiB · 🔺 +690 B (+0.0%)

Playwright — all passed

All tests passed.

View test results →

⚠️ Backend coverage — 98.0% of changed backend lines covered — 6 uncovered

🧪 Backend test coverage

Patch coverage — changed backend lines (products + core): ████████████████████ 98.0% (385 / 391)

File Patch Uncovered changed lines
products/warehouse_sources/backend/temporal/data_imports/sources/heroku/source.py 94.9% 75, 79
products/warehouse_sources/backend/temporal/data_imports/sources/heroku/heroku.py 97.3% 100, 187, 194
products/warehouse_sources/backend/temporal/data_imports/sources/heroku/tests/test_heroku_source.py 98.6% 115

🤖 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 29457667443 -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.4% 25,560 / 37,895
signals ████████████████░░░░ 79.1% 19,031 / 24,073
data_modeling ████████████████░░░░ 80.0% 4,834 / 6,045
cdp ████████████████░░░░ 80.7% 3,118 / 3,864
notebooks █████████████████░░░ 84.3% 6,343 / 7,520
agent_platform █████████████████░░░ 84.7% 3,273 / 3,862
cohorts █████████████████░░░ 86.1% 4,022 / 4,671
actions █████████████████░░░ 86.6% 717 / 828
product_tours █████████████████░░░ 87.5% 1,266 / 1,447
exports ██████████████████░░ 88.3% 6,891 / 7,800
conversations ██████████████████░░ 88.9% 16,129 / 18,133
dashboards ██████████████████░░ 89.1% 5,719 / 6,418
mcp_analytics ██████████████████░░ 89.1% 2,502 / 2,807
error_tracking ██████████████████░░ 89.6% 9,718 / 10,852
alerts ██████████████████░░ 89.9% 3,638 / 4,046
engineering_analytics ██████████████████░░ 90.1% 5,105 / 5,665
streamlit_apps ██████████████████░░ 90.4% 2,499 / 2,764
slack_app ██████████████████░░ 90.6% 9,511 / 10,503
marketing_analytics ██████████████████░░ 90.8% 11,514 / 12,684
product_analytics ██████████████████░░ 91.1% 5,599 / 6,143
data_warehouse ██████████████████░░ 92.1% 18,133 / 19,683
workflows ██████████████████░░ 92.4% 5,148 / 5,574
web_analytics ███████████████████░ 92.7% 13,624 / 14,691
ai_observability ███████████████████░ 92.8% 14,868 / 16,019
surveys ███████████████████░ 92.9% 5,687 / 6,120
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
skills ███████████████████░ 94.4% 2,827 / 2,995
revenue_analytics ███████████████████░ 94.5% 3,598 / 3,809
review_hog ███████████████████░ 94.6% 6,532 / 6,905
logs ███████████████████░ 95.3% 9,528 / 9,994
experiments ███████████████████░ 95.6% 24,171 / 25,288
replay_vision ███████████████████░ 95.7% 13,354 / 13,952
annotations ███████████████████░ 96.2% 732 / 761
warehouse_sources ███████████████████░ 96.2% 230,633 / 239,669
feature_flags ███████████████████░ 96.3% 16,002 / 16,625
user_interviews ███████████████████░ 96.4% 2,242 / 2,325
data_catalog ███████████████████░ 97.1% 2,034 / 2,095
customer_analytics ███████████████████░ 97.2% 7,480 / 7,698

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.

@veria-ai

veria-ai Bot commented Jul 14, 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: 2 · PR risk: 0/10

@trunk-io

trunk-io Bot commented Jul 14, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

Copy link
Copy Markdown
Member Author

Pushed 62ba49e addressing CI and review feedback:

  • mypy: HTTPError in the test helper now passes response= (the requests stubs require it).
  • Security review finding: capability URLs are now nulled before rows are yielded — output_stream_url on builds and releases, source_blob.url on builds, and attach_url on dynos. These grant access (source downloads, output streams, dyno attach) without Heroku auth, so they must not be queryable in the warehouse. Declared per endpoint in settings.py (sensitive_fields, dotted paths), applied centrally in the paginator, covered by parameterized tests, and reflected in the canonical column descriptions.

The deploy preview failure is an infra timeout (httpx.ReadTimeout while bringing up the preview environment), unrelated to this diff.

Comment thread products/warehouse_sources/backend/temporal/data_imports/sources/heroku/heroku.py Outdated
@posthog

posthog Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

👋 Visual changes detected for this PR.

Review and approve in PostHog Visual Review

If these changes are unexpected, they may be caused by a flaky test or a broken snapshot on master. Don't approve — rerun the job or wait for a fix.

@danielcarletti danielcarletti left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Implements the Heroku warehouse source: 11 endpoints (apps, addons, builds, collaborators, domains, dynos, formation, invoices, pipelines, releases, teams) over the Platform API v3 with Range-header cursor pagination, per-app fan-out, resumable imports, canonical table descriptions, an icon, and tests.

Generated-By: PostHog Code
Task-Id: 7fee9ef6-177a-4459-96b3-def090128b08
Nulls capability URLs before rows are yielded (build/release output_stream_url, build source_blob.url, dyno attach_url) — these grant access without Heroku auth and must not land in the warehouse. Also passes response= to HTTPError in the test helper to satisfy the requests stubs.

Generated-By: PostHog Code
Task-Id: 7fee9ef6-177a-4459-96b3-def090128b08
Several Heroku endpoints return capability URLs carrying secrets (builds'
`source_blob.url`, dynos' `rendezvous://.../secret` `attach_url`) that the
name-based sample scrubbers can't recognise. `_redact_sensitive_fields` only
scrubs the yielded rows, not the raw response the tracked transport would
otherwise capture, so disable capture on the shared session.

Generated-By: PostHog Code
Task-Id: ba6dcc2f-13fe-4967-8c56-2a17a1cf1fa1
Remove unreleasedSource=True so the finished source is visible in the
connector catalog, and drop any test asserting the hidden state.
@Gilbert09
Gilbert09 force-pushed the posthog-code/heroku-warehouse-source branch from 9e4360d to 6580a5b Compare July 15, 2026 23:09
@talyn-app
talyn-app Bot merged commit f91d7a4 into master Jul 15, 2026
257 checks passed
@talyn-app
talyn-app Bot deleted the posthog-code/heroku-warehouse-source branch July 15, 2026 23:52
@deployment-status-posthog

deployment-status-posthog Bot commented Jul 16, 2026

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-07-16 00:21 UTC Run
prod-us ✅ Deployed 2026-07-16 00:40 UTC Run
prod-eu ✅ Deployed 2026-07-16 00:41 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