Skip to content

feat(data-warehouse): implement openai import source#70722

Merged
Gilbert09 merged 3 commits into
masterfrom
posthog-code/openai-warehouse-source
Jul 15, 2026
Merged

feat(data-warehouse): implement openai import source#70722
Gilbert09 merged 3 commits into
masterfrom
posthog-code/openai-warehouse-source

Conversation

@Gilbert09

Copy link
Copy Markdown
Member

Problem

OpenAI is the default LLM provider for most companies, so organization-level API spend and usage history is one of the most requested AI datasets to land in the warehouse. The openai source existed only as a scaffolded stub (registered, unreleased, no fields or sync logic).

Changes

Implements the OpenAI import source end-to-end, following the same architecture as the Anthropic source (whose admin API closely mirrors OpenAI's):

  • settings.py – declarative endpoint catalog: 18 tables covering the Usage API (usage_completions, usage_embeddings, usage_moderations, usage_images, usage_audio_speeches, usage_audio_transcriptions, usage_vector_stores, usage_code_interpreter_sessions), the Costs API (costs), org dimension tables (projects, users, invites, admin_api_keys), project fan-out tables (project_users, project_service_accounts, project_api_keys, project_rate_limits), and audit_logs.
  • openai.py – transport: bearer-auth GETs via make_tracked_session(), tenacity backoff on 429/5xx, page-token pagination for the bucketed usage/costs endpoints and after-cursor pagination for entity lists, resumable state saved after each yielded batch.
  • source.pyResumableSource wiring: single Admin API key field, static schema catalog (lists_tables_without_credentials), credential validation (401 rejects, 403 accepted at create time), non-retryable 401/403 messages.
  • canonical_descriptions.py – table/column descriptions from the official API docs for all 18 tables, feeding semantic enrichment and the public docs table list.

Sync semantics:

  • Usage/costs are incremental on start_time (server-side Unix-seconds filter) with a trailing-day lookback, since OpenAI restates a day's bucket as late usage lands. Rows are one-per-(bucket × group-by combination) with a synthesized hash id over the identity dimensions only, so restated buckets merge in place.
  • Audit logs are incremental on effective_at via the server-side effective_at[gte] filter. They return newest-first with no order param, so the endpoint declares sort_mode="desc" and the watermark only commits at sync completion.
  • Entity lists have no updated-since filter, so they are full refresh only.

Note

Conservative choices where live verification wasn't possible (I had no real Admin API key to probe with, endpoints answer 401 without one): bucket ordering for usage endpoints was verified oldest-first against the OpenAI cookbook's pagination example rather than a live call, the wire format for group_by (repeated params) and effective_at[gte] (bracket style) matches the official SDK's query serialization, and the documented-in-the-wild "empty page with a next_page token" quirk on /organization/costs is guarded by stopping pagination on an empty page.

The source stays behind unreleasedSource=True with releaseStatus=ALPHA while it awaits a live-credential shakedown; flipping it visible is a one-line follow-up.

User-facing doc: PostHog/posthog.com#18463 (docsUrl here matches its slug; audit_source_docs passes for this source against that checkout).

How did you test this code?

Automated tests only (no live API credentials were used):

  • tests/test_openai.py (transport): bucket pagination follows/resumes page tokens and stops on the empty-page costs quirk (would otherwise loop forever); entity cursor pagination advances on after and falls back to the last item id when last_id is absent (would otherwise silently truncate to one page); audit-log incremental runs send the effective_at[gte] filter (without it every sync refetches all history); surrogate-id stability across metric restatements (a change would duplicate rows on every merge); owner flattening for both API key shapes; credential status mapping (200/403 valid, 401 invalid); retry budget for 429/5xx vs no retry for 4xx.
  • tests/test_openai_source.py (source class): schema catalog completeness, incremental flags only on endpoints with genuine server-side filters, composite primary keys on fan-out children (a bare id key would multi-match on merge and OOM), sort_mode="desc" on audit logs, documented-tables output for public docs.

All 63 tests pass via hogli test; ruff check/ruff format and hogli ci:preflight --fix are clean. makemigrations --check reports no changes (the enum already existed from scaffolding).

Automatic notifications

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

Docs update

Doc added in PostHog/posthog.com#18463.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Authored with PostHog Code (Claude). Skills invoked: /implementing-warehouse-sources, /documenting-warehouse-sources, /writing-tests. Decisions: modeled on the Anthropic source (OpenAI's admin API is its close analog) rather than the generic rest_source.RESTClient, because the bucketed usage endpoints need row synthesis and resumable page-token state that the sibling source already solves; kept the scope to Admin-API-key endpoints and left project-key datasets (batches, fine-tuning jobs, files) out, since they'd need a second credential and the headline datasets are org usage/spend; regenerated generated_configs.py and reverted an unrelated environment-dependent Stripe hunk the generator produced locally.


Created with PostHog Code

@Gilbert09 Gilbert09 self-assigned this Jul 14, 2026
@assign-reviewers-posthog assign-reviewers-posthog Bot requested a review from a team July 14, 2026 13:11
@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. 🙂

@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 54cb3f1 · box box-fdb1aa35f04d · ready in 1161s (push → usable) · build log · rebuilds on every push, torn down on close

@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.79 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.14 MiB · 2,979 files no change █████████░ 88.0% 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.1 KiB ../node_modules/.pnpm/posthog-js@1.401.0/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.7 KiB ../node_modules/.pnpm/posthog-js@1.401.0/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
106.1 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 — 🔺 +1.6 KiB (+0.0%)

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

Total: 1308.69 MiB · 🔺 +1.6 KiB (+0.0%)

⚠️ Playwright — 1 failed

🎭 Playwright report · View test results →

1 failed test:

  • password-protected insight sharing (chromium)

These issues are not necessarily caused by your changes.
Annoyed by this section? Help fix flakies and failures and it will go green!

⚠️ Backend coverage — 96.0% of changed backend lines covered — 19 uncovered

🧪 Backend test coverage

Patch coverage — changed backend lines (products + core): ███████████████████░ 96.0% (562 / 581)

File Patch Uncovered changed lines
products/warehouse_sources/backend/temporal/data_imports/sources/openai/source.py 90.6% 114–115, 117
products/warehouse_sources/backend/temporal/data_imports/sources/openai/openai.py 92.2% 58–60, 67, 107, 167, 260, 324–326, 344–346, 358–360

🤖 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 29427991659 -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,473 / 37,804
signals ████████████████░░░░ 79.1% 19,074 / 24,111
data_modeling ████████████████░░░░ 80.0% 4,834 / 6,045
cdp ████████████████░░░░ 80.7% 3,118 / 3,864
agent_platform █████████████████░░░ 84.2% 3,112 / 3,695
notebooks █████████████████░░░ 84.3% 6,323 / 7,501
cohorts █████████████████░░░ 86.0% 3,989 / 4,639
actions █████████████████░░░ 86.6% 717 / 828
product_tours █████████████████░░░ 87.5% 1,266 / 1,447
exports ██████████████████░░ 88.3% 6,891 / 7,800
conversations ██████████████████░░ 88.9% 16,126 / 18,130
dashboards ██████████████████░░ 89.0% 5,647 / 6,345
mcp_analytics ██████████████████░░ 89.1% 2,502 / 2,807
error_tracking ██████████████████░░ 89.5% 9,691 / 10,824
alerts ██████████████████░░ 89.9% 3,638 / 4,046
engineering_analytics ██████████████████░░ 90.0% 5,065 / 5,625
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,527 / 6,068
data_warehouse ██████████████████░░ 92.1% 17,915 / 19,459
workflows ██████████████████░░ 92.4% 5,148 / 5,574
web_analytics ███████████████████░ 92.7% 13,607 / 14,674
ai_observability ███████████████████░ 92.8% 14,863 / 16,014
surveys ███████████████████░ 92.9% 5,660 / 6,094
posthog_ai ███████████████████░ 93.2% 1,312 / 1,408
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,475 / 6,848
logs ███████████████████░ 95.3% 9,528 / 9,994
experiments ███████████████████░ 95.6% 24,138 / 25,244
replay_vision ███████████████████░ 95.7% 13,300 / 13,896
feature_flags ███████████████████░ 96.1% 14,750 / 15,354
warehouse_sources ███████████████████░ 96.2% 222,397 / 231,294
annotations ███████████████████░ 96.2% 732 / 761
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.

@trunk-io

trunk-io Bot commented Jul 14, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

@github-actions github-actions Bot requested a deployment to preview-pr-70722 July 14, 2026 16:18 In progress

@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

Copy link
Copy Markdown
Member Author

Status

Review feedback — resolved. The project fan-out path was advancing the resume checkpoint to the next project while the current project's rows could still be buffered in the batcher (flushed only at the very end), so a crash in between would lose them on resume. Fixed by flushing incomplete batches before checkpointing, with a regression test (test_flushes_each_projects_rows_before_checkpointing_next).

CI — all real checks are green. The only red check is deploy preview, which fails on hogbox preview-environment provisioning, not on this PR's code. I re-ran it three times and each attempt hit a different transient hogland infra error at a different stage:

  1. httpx.ReadTimeout on POST /v1/hogboxes
  2. HTTP 500 placement failed: … chunkfs mount: chunkfsd ready: not ready within 5s
  3. HTTP 401 unauthorized while swapping in the PR frontend

These are all preview-provisioning infrastructure failures — the diff here is a data-warehouse backend source plus an openai.svg icon, which can't affect box placement or OIDC. The workflow itself notes it retries on the next push, so a later push should clear it.

Merge — the branch is conflict-free and up to date; the remaining BLOCKED state is REVIEW_REQUIRED, i.e. it needs a human approval, which I can't provide.

🦉 via talyn.dev

Fill in the scaffolded OpenAI warehouse source: usage/costs bucketed
endpoints, org/project entity tables, project fan-out, and audit logs,
following the anthropic source pattern (ResumableSource + tracked HTTP).

Generated-By: PostHog Code
Task-Id: a318eafb-08f9-49ad-b05c-2868535919ad
…eckpoint

In the project fan-out path, the resume state was advanced to the next
project while the current project's rows could still be buffered in the
batcher, which is only flushed at the very end. A crash between the state
save and the final flush would drop those buffered rows on resume, since
resume restarts at the checkpointed next project.

Flush any incomplete batch before checkpointing to the next project, and
add a regression test asserting a project's rows are yielded before the
cursor advances.

Generated-By: PostHog Code
Task-Id: f647c7e7-718a-43ed-8283-01c68be8944c
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/openai-warehouse-source branch from 85b456f to 54cb3f1 Compare July 15, 2026 15:25
@Gilbert09 Gilbert09 merged commit d33019a into master Jul 15, 2026
254 checks passed
@Gilbert09 Gilbert09 deleted the posthog-code/openai-warehouse-source branch July 15, 2026 22:35
@deployment-status-posthog

deployment-status-posthog Bot commented Jul 15, 2026

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-07-15 23:16 UTC Run
prod-us ✅ Deployed 2026-07-15 23:32 UTC Run
prod-eu ✅ Deployed 2026-07-15 23:34 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