Files
zeekay bc4ddf332b chore: rebase the tree onto upstream master fa1a9a60
Mechanically re-derived: bin/debrand runs over a fresh import of
posthog/posthog@fa1a9a60d4 (2026-08-06).
The pipeline (.hanzo/workflows), the Dockerfiles and bin/docker*
entrypoints, compose files, LLM.md and the squashed initial migration
are restored from the shipped tree, same as the previous import.

The prune's import-closure guard kept 21 products the new core now
imports; dropping them is a refactor now, recorded rather than forced.
2026-08-06 01:25:36 -07:00

388 lines
15 KiB
TypeScript

import { MakeLogicType, actions, afterMount, kea, key, listeners, path, props, reducers, selectors } from 'kea'
import { loaders } from 'kea-loaders'
import { router, urlToAction } from 'kea-router'
import insights from 'insights-js'
import { toast } from 'lib/elements/Toast'
import { getCurrentTeamId } from 'lib/utils/getAppContext'
import { sceneConfigurations } from 'scenes/scenes'
import { Scene } from 'scenes/sceneTypes'
import { urls } from 'scenes/urls'
import { Breadcrumb } from '~/types'
import {
subscriptionsDeliveriesList,
subscriptionsPartialUpdate,
subscriptionsRetrieve,
subscriptionsTestDeliveryCreate,
} from 'products/subscriptions/frontend/generated/api'
import type {
PaginatedSubscriptionDeliveryListApi,
SubscriptionApi,
SubscriptionsDeliveriesListStatus,
} from 'products/subscriptions/frontend/generated/api.schemas'
import { runSubscriptionTestDelivery } from '../components/Subscriptions/runSubscriptionTestDelivery'
import { subscriptionName } from './components/SubscriptionsTable'
export type SubscriptionSceneLogicProps = {
id: string
}
export type DeliveryFeedback = 'positive' | 'negative'
export type DeliveryFeedbackSource = 'email' | 'slack' | 'in_app'
export const FEEDBACK_THANKS_DISPLAY_MS = 1000
function parseCursorFromPaginationUrl(url: string | null | undefined): string | undefined {
if (!url) {
return undefined
}
try {
const parsed = new URL(url, typeof window !== 'undefined' ? window.location.origin : 'http://localhost')
const cursor = parsed.searchParams.get('cursor')
return cursor ?? undefined
} catch {
return undefined
}
}
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
export interface subscriptionSceneLogicValues {
breadcrumbs: Breadcrumb[]
deliveriesPage: PaginatedSubscriptionDeliveryListApi | null
deliveriesPageLoading: boolean
deliveringSubscriptionId: number | null
deliveryFeedback: Record<string, DeliveryFeedback>
deliveryStatusFilter: SubscriptionsDeliveriesListStatus | null
recentlyThankedDeliveries: Record<string, true>
subscription: SubscriptionApi | null
subscriptionLoading: boolean
}
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
export interface subscriptionSceneLogicActions {
deliverSubscription: (id: number) => {
id: number
}
deliverSubscriptionFailure: () => {
value: true
}
deliverSubscriptionSuccess: () => {
value: true
}
expireDeliveryThanks: (deliveryId: string) => {
deliveryId: string
}
loadDeliveriesPage: (targetUrl: string | null) => string | null
loadDeliveriesPageFailure: (
error: string,
errorObject?: any
) => {
error: string
errorObject?: any
}
loadDeliveriesPageSuccess: (
deliveriesPage: PaginatedSubscriptionDeliveryListApi | null,
payload?: string | null
) => {
deliveriesPage: PaginatedSubscriptionDeliveryListApi | null
payload?: string | null
}
loadSubscription: () => any
loadSubscriptionFailure: (
error: string,
errorObject?: any
) => {
error: string
errorObject?: any
}
loadSubscriptionSuccess: (
subscription: SubscriptionApi | null,
payload?: any
) => {
subscription: SubscriptionApi | null
payload?: any
}
setDeliveryStatusFilter: (status: SubscriptionsDeliveriesListStatus | null) => {
status: SubscriptionsDeliveriesListStatus | null
}
setEnabled: ({ enabled }: { enabled: boolean }) => {
enabled: boolean
}
setEnabledFailure: (
error: string,
errorObject?: any
) => {
error: string
errorObject?: any
}
setEnabledSuccess: (
subscription: SubscriptionApi | null,
payload?: {
enabled: boolean
}
) => {
subscription: SubscriptionApi | null
payload?: {
enabled: boolean
}
}
submitDeliveryFeedback: (
deliveryId: string,
feedback: DeliveryFeedback,
source: DeliveryFeedbackSource
) => {
deliveryId: string
feedback: DeliveryFeedback
source: DeliveryFeedbackSource
}
}
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
export interface subscriptionSceneLogicMeta {
key: string
__keaTypeGenInternalSelectorTypes: {
breadcrumbs: (subscription: SubscriptionApi | null) => Breadcrumb[]
}
}
export type subscriptionSceneLogicType = MakeLogicType<
subscriptionSceneLogicValues,
subscriptionSceneLogicActions,
SubscriptionSceneLogicProps,
subscriptionSceneLogicMeta
>
export const subscriptionSceneLogic = kea<subscriptionSceneLogicType>([
props({} as SubscriptionSceneLogicProps),
key(({ id }) => id),
path((key) => ['scenes', 'subscriptions', 'subscriptionSceneLogic', key]),
actions({
deliverSubscription: (id: number) => ({ id }),
deliverSubscriptionSuccess: true,
deliverSubscriptionFailure: true,
setDeliveryStatusFilter: (status: SubscriptionsDeliveriesListStatus | null) => ({ status }),
submitDeliveryFeedback: (deliveryId: string, feedback: DeliveryFeedback, source: DeliveryFeedbackSource) => ({
deliveryId,
feedback,
source,
}),
expireDeliveryThanks: (deliveryId: string) => ({ deliveryId }),
}),
reducers({
deliveringSubscriptionId: [
null as number | null,
{
deliverSubscription: (_, { id }) => id,
deliverSubscriptionSuccess: () => null,
deliverSubscriptionFailure: () => null,
},
],
deliveryStatusFilter: [
null as SubscriptionsDeliveriesListStatus | null,
{
setDeliveryStatusFilter: (_, { status }) => status,
},
],
deliveryFeedback: [
{} as Record<string, DeliveryFeedback>,
// localStorage on purpose: feedback is an analytics event, not a DB row — this only keeps
// the UI honest (and the capture deduped) across reloads on this browser.
{ persist: true },
{
submitDeliveryFeedback: (state, { deliveryId, feedback }) => ({ ...state, [deliveryId]: feedback }),
},
],
// Transient (not persisted): drives the brief "Thanks!" flash before the row settles
// into showing the recorded option.
recentlyThankedDeliveries: [
{} as Record<string, true>,
{
submitDeliveryFeedback: (state, { deliveryId }) => ({ ...state, [deliveryId]: true as const }),
expireDeliveryThanks: (state, { deliveryId }) => {
const { [deliveryId]: _removed, ...rest } = state
return rest
},
},
],
}),
loaders(({ values, props }) => ({
subscription: [
null as SubscriptionApi | null,
{
loadSubscription: async () => {
const numericId = parseInt(props.id, 10)
if (!Number.isFinite(numericId)) {
return null
}
return await subscriptionsRetrieve(String(getCurrentTeamId()), numericId)
},
// Used by the Pause / Resume button on the detail page. PATCH
// returns the updated subscription so the loader replaces the
// whole value — no extra GET needed.
setEnabled: async ({ enabled }: { enabled: boolean }) => {
const numericId = parseInt(props.id, 10)
if (!Number.isFinite(numericId)) {
return values.subscription
}
return await subscriptionsPartialUpdate(String(getCurrentTeamId()), numericId, { enabled })
},
},
],
deliveriesPage: [
null as PaginatedSubscriptionDeliveryListApi | null,
{
loadDeliveriesPage: async (targetUrl: string | null) => {
const numericId = parseInt(props.id, 10)
if (!Number.isFinite(numericId)) {
return null
}
const teamId = String(getCurrentTeamId())
const status = values.deliveryStatusFilter ?? undefined
if (targetUrl === null) {
return await subscriptionsDeliveriesList(teamId, numericId, status ? { status } : {})
}
const cursor = parseCursorFromPaginationUrl(targetUrl)
return await subscriptionsDeliveriesList(teamId, numericId, {
...(cursor ? { cursor } : {}),
...(status ? { status } : {}),
})
},
},
],
})),
selectors({
breadcrumbs: [
(s) => [s.subscription],
(subscription: SubscriptionApi | null): Breadcrumb[] => {
const listCrumb: Breadcrumb = {
key: Scene.Subscriptions,
name: sceneConfigurations[Scene.Subscriptions].name,
path: urls.subscriptions(),
iconType: sceneConfigurations[Scene.Subscriptions].iconType || 'default_icon_type',
}
if (!subscription) {
return [listCrumb]
}
return [
listCrumb,
{
key: [Scene.Subscription, subscription.id],
name: subscriptionName(subscription),
iconType: sceneConfigurations[Scene.Subscription].iconType || 'default_icon_type',
},
]
},
],
}),
listeners(({ actions, values, props, cache, selectors }) => ({
submitDeliveryFeedback: ({ deliveryId, feedback, source }, _breakpoint, _action, previousState) => {
insights.capture('ai_report_feedback', {
subscription_id: parseInt(props.id, 10),
delivery_id: deliveryId,
feedback,
source,
// Lets analysis distinguish first votes from switches; consumers take the latest
// event per person + delivery, so a switched vote simply wins.
previous_feedback: selectors.deliveryFeedback(previousState)[deliveryId] ?? null,
})
// In-app thumbs show a per-row "Thanks" state instead of a toast.
if (source !== 'in_app') {
toast.success('Thanks for your feedback')
}
// Per-delivery key so spamming replaces the previous timer instead of stacking.
cache.disposables.add(() => {
const timerId = setTimeout(() => actions.expireDeliveryThanks(deliveryId), FEEDBACK_THANKS_DISPLAY_MS)
return () => clearTimeout(timerId)
}, `deliveryThanks-${deliveryId}`)
},
setDeliveryStatusFilter: () => {
if (values.subscription) {
void actions.loadDeliveriesPage(null)
}
},
loadSubscriptionSuccess: () => {
if (values.subscription) {
void actions.loadDeliveriesPage(null)
}
},
deliverSubscription: async ({ id }) => {
const result = await runSubscriptionTestDelivery(() =>
subscriptionsTestDeliveryCreate(String(getCurrentTeamId()), id)
)
if (result === 'success') {
actions.deliverSubscriptionSuccess()
} else {
actions.deliverSubscriptionFailure()
}
},
// Test delivery returns 202 before Temporal persists the delivery row. Refetch now (fast path) and
// again after a short delay so the list usually shows the new row without manual refresh.
deliverSubscriptionSuccess: async (_, breakpoint) => {
void actions.loadDeliveriesPage(null)
await breakpoint(2000)
void actions.loadDeliveriesPage(null)
},
loadDeliveriesPageFailure: ({ errorObject }) => {
const status = errorObject?.status
const detail = errorObject?.detail
const message =
status === 404
? 'Delivery history is not available.'
: typeof detail === 'string'
? detail
: 'Could not load delivery history.'
toast.error(message)
},
setEnabledSuccess: ({ subscription }) => {
if (!subscription) {
return
}
toast.success(subscription.enabled ? 'Subscription enabled' : 'Subscription disabled')
},
setEnabledFailure: ({ errorObject }) => {
// Surface the serializer's actionable message (e.g. re-enabling a Slack
// sub with no integration) — backend already validates this.
const detail = errorObject?.detail
toast.error(typeof detail === 'string' ? detail : 'Could not update subscription')
},
})),
urlToAction(({ actions, props, values }) => ({
// Links in delivered reports land here with these params; capture once,
// then strip them so a refresh doesn't double-capture.
[urls.subscription(':id')]: ({ id }, searchParams) => {
if (id !== props.id) {
return
}
const { feedback_delivery, feedback, feedback_source, delivery, ...restSearchParams } = searchParams
const clickedDelivery = feedback_delivery ?? delivery
if (!clickedDelivery) {
return
}
const source = feedback_source === 'slack' ? 'slack' : 'email'
// Delivery engagement is a proxy for the report being read — subscribing alone is a weak signal.
insights.capture('ai_report_clicked', {
subscription_id: parseInt(props.id, 10),
delivery_id: String(clickedDelivery),
link: feedback_delivery ? 'feedback' : 'manage',
source,
})
if (feedback_delivery && (feedback === 'positive' || feedback === 'negative')) {
const deliveryId = String(feedback_delivery)
if (values.deliveryFeedback[deliveryId]) {
// Persisted state remembers this delivery — don't re-capture from a re-clicked link.
toast.info('Your feedback for this report was already recorded')
} else {
actions.submitDeliveryFeedback(deliveryId, feedback, source)
}
}
router.actions.replace(router.values.location.pathname, restSearchParams, router.values.hashParams)
},
})),
afterMount(({ actions }) => {
void actions.loadSubscription()
}),
])