feat(subscriptions): AI subscription API, validation, telemetry, and UI#59632
feat(subscriptions): AI subscription API, validation, telemetry, and UI#59632vdekrijger wants to merge 2 commits into
Conversation
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
|
🎭 Playwright didn't run on this PR — your changes touch code that could affect E2E behavior, but Playwright is opt-in via label now to keep CI cost down. Add the Most PRs don't need this. Real regressions still get caught on master and fix-forward. |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Size Change: +5.2 kB (0%) Total Size: 119 MB 📦 View Changed
ℹ️ View Unchanged
|
🔍 Migration Risk AnalysisWe've analyzed your migrations for potential risks. Summary: 0 Safe | 0 Needs Review | 1 Blocked ❌ BlockedCauses locks or breaks compatibility 📚 How to Deploy These Changes SafelyAddField: This operation acquires a brief lock but doesn't rewrite the table. Deployment uses lock timeouts with automatic retries, so lock contention will cause retries rather than connection pile-up. RunPython: Use batching for large data migrations:
See the migration safety guide Last updated: 2026-05-27 07:48 UTC (af2c303) |
|
👋 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. |
| _AI_CONFIG_ALLOWED_KEYS = frozenset({"model", "planner_model"}) | ||
|
|
||
|
|
||
| def _validate_ai_config_dict(value): |
There was a problem hiding this comment.
As mentioned this and the AI_CONFIG_ALLOWED_KEYS should all be removed as I think we should remove that field to begin with.
There was a problem hiding this comment.
Similar to the ai config fields below, will ignore flagging them and consider this one to be the main signal.
There was a problem hiding this comment.
Done in af2c303 — removed the API-layer ai_config usage and the AI_CONFIG_ALLOWED_KEYS import. The model field + migration removal belongs in #59634 (where it was added), so I scoped that out of this PR.
|
|
||
|
|
||
| def _ai_create_gate_reason(organization, *, kind: str = "subscriptions", verb: str = "creating") -> Optional[str]: | ||
| """Returns the human-readable reason why creating an AI subscription / ad-hoc AI |
There was a problem hiding this comment.
Please simplify this doc! It's a bit too verbose.
There was a problem hiding this comment.
Simplified in af2c303 — trimmed the docstring/comment down to the essential rationale.
|
|
||
|
|
||
| class AiReportRequestSerializer(serializers.Serializer): | ||
| """Input for the ad-hoc AI report endpoint — same prompt validation as a scheduled AI subscription.""" |
There was a problem hiding this comment.
Wait what is this? Why do we have an ad hoc AI report endpoint? why isn't everything part of the prompt / scheduled sub with a possibility to "fire off now"?
There was a problem hiding this comment.
Removed in af2c303 — dropped the ad-hoc ai_report action and its serializers entirely. Everything now goes through the scheduled prompt subscription path.
| if existing is None and "content_type" not in attrs: | ||
| attrs["content_type"] = content_type | ||
|
|
||
| if content_type == Subscription.ContentType.INSIGHT: |
There was a problem hiding this comment.
This entire validation fiesta is new right? I think we can do better here to avoid the if elif stuff that it's currently looking like. Also I think that the content type fallbacks might not be needed as we just need ot ensure it's always set in the migration PR.
There was a problem hiding this comment.
Refactored in af2c303 — replaced the if/elif chain with a dict-dispatch on content_type (one validator per type) plus a fail-soft fallback that returns a 400 instead of a 500 on an unexpected value (e.g. a stale row).
| # on the first scheduled run is a poor first impression — fail fast here. | ||
| effective_target_type = attrs.get("target_type") or (existing.target_type if existing else None) | ||
| if effective_target_type and effective_target_type not in ( | ||
| Subscription.SubscriptionTarget.EMAIL, |
There was a problem hiding this comment.
I thought we already had checks like this at creation time?
There was a problem hiding this comment.
Confirmed — folded into the create-time gate in af2c303 rather than duplicating the check.
| </LemonBanner> | ||
| )} | ||
| <LemonBanner type="info" className="text-sm"> | ||
| The AI plans up to 3 HogQL queries against your project's events and writes a |
There was a problem hiding this comment.
Does it make sense to tell these implementation details:? if it ever deviates, this risks becoming outdated, I wonder if we can instead keep it more abstract?
There was a problem hiding this comment.
Agreed — abstracted in af2c303. The banner copy no longer names pipeline internals (HogQL / query count) so it won't go stale if the implementation changes.
| * (turns red at the cap), so we don't add our own. Example chips sit beneath | ||
| * the textarea on their own row so they wrap cleanly without competing with | ||
| * the counter for horizontal space. | ||
| */} |
There was a problem hiding this comment.
No need for this comment.
There was a problem hiding this comment.
Removed in af2c303.
| ), | ||
| render: (_value: unknown, sub: SubscriptionApi) => { | ||
| // All three kinds render as neutral grey LemonTags differentiated by icon, not | ||
| // colour — status/accent colours (red, green, purple) would read as |
There was a problem hiding this comment.
This comment feels unneeded as well
There was a problem hiding this comment.
Removed in af2c303.
| Mine = 'mine', | ||
| Dashboard = 'dashboard', | ||
| Insight = 'insight', | ||
| Ai = 'ai_prompt', |
There was a problem hiding this comment.
Please capitalise the I in AI as well.
There was a problem hiding this comment.
Done in af2c303 — Ai → AI across the enum and its references.
| @@ -0,0 +1,567 @@ | |||
| /** | |||
| * Auto-generated from the Django backend OpenAPI schema. | |||
There was a problem hiding this comment.
These files indicate that we had no MCP behaviour yet for the subscription flow? Or what is the goal of all these auto generated files?
There was a problem hiding this comment.
These are the auto-generated API types (Orval from the OpenAPI spec) for the subscriptions product module, regenerated here because the serializer changed — not hand-edited. They're produced by hogli build:openapi. No MCP behaviour changed in this PR.
Fixes from the review-swarm pass: fail-soft content_type dispatch (400 not 500 on unexpected values), persist trimmed prompt, add type annotations to validate() helpers, field-keyed validator errors, SLO content_type assertion guard, stale comment cleanup.
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|

Problem
Changes
How did you test this code?
👉 Stay up-to-date with PostHog coding conventions for a smoother review.
Publish to changelog?
Docs update
🤖 Agent context