Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions supabase/functions/_backend/triggers/canceled_org_retention_alerts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import type { MiddlewareKeyVariables } from '../utils/hono.ts'
import { Hono } from 'hono/tiny'
import { BRES, middlewareAPISecret, parseBody, simpleError } from '../utils/hono.ts'
import { cloudlog } from '../utils/logging.ts'
import { sendEventToTracking } from '../utils/tracking.ts'

type RetentionAlertType = 'bundles_deletion_warning' | 'app_deletion_warning'

interface CanceledOrgRetentionAlertPayload {
org_id: string
org_name?: string
management_email?: string
alert_type: RetentionAlertType
access_end?: string
days_until_deletion?: number
app_ids?: string[]
}

const ALERT_CONFIG = {
bundles_deletion_warning: {
bentoEvent: 'org:bundles_will_be_deleted',
trackingEvent: 'Bundles will be deleted',
icon: '📦',
},
app_deletion_warning: {
bentoEvent: 'org:apps_will_be_deleted',
trackingEvent: 'Apps will be deleted',
icon: '🗑️',
},
} as const satisfies Record<RetentionAlertType, {
bentoEvent: string
trackingEvent: string
icon: string
}>

function isRetentionAlertType(value: unknown): value is RetentionAlertType {
return value === 'bundles_deletion_warning' || value === 'app_deletion_warning'
}

function accessEndDateKey(accessEnd: string | undefined): string {
if (!accessEnd)
return 'unknown'
const parsed = new Date(accessEnd)
Comment thread
cursor[bot] marked this conversation as resolved.
if (Number.isNaN(parsed.getTime()))
return accessEnd.slice(0, 10)
return parsed.toISOString().slice(0, 10)
}

export const app = new Hono<MiddlewareKeyVariables>()

app.post('/', middlewareAPISecret, async (c) => {
const payload = await parseBody<CanceledOrgRetentionAlertPayload>(c)
const orgId = payload.org_id
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
const alertType = payload.alert_type

if (!orgId || !isRetentionAlertType(alertType))
throw simpleError('invalid_payload', 'Missing org_id or alert_type in retention alert payload', { payload })

const config = ALERT_CONFIG[alertType]

const parsedDays = Number(payload.days_until_deletion ?? 5)
const daysUntilDeletion = Number.isFinite(parsedDays) ? parsedDays : 5
const appIds = Array.isArray(payload.app_ids) ? payload.app_ids : []
const accessEndKey = accessEndDateKey(payload.access_end)
const uniqId = `retention:${alertType}:${accessEndKey}`

const metadata = {
org_id: orgId,
org_name: payload.org_name ?? '',
management_email: payload.management_email ?? '',
alert_type: alertType,
access_end: payload.access_end ?? '',
days_until_deletion: daysUntilDeletion,
app_ids: appIds,
app_count: appIds.length,
}

cloudlog({
requestId: c.get('requestId'),
message: 'canceled org retention alert',
eventName: config.bentoEvent,
orgId,
alertType,
daysUntilDeletion,
appCount: appIds.length,
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

await sendEventToTracking(c, {
bento: {
once: true,
data: metadata,
event: config.bentoEvent,
preferenceKey: 'usage_limit',
uniqId,
audience: 'billing',
},
channel: 'usage',
event: config.trackingEvent,
icon: config.icon,
user_id: orgId,
groups: { organization: orgId },
notify: false,
sentToBento: true,
tags: {
alert_type: alertType,
days_until_deletion: String(daysUntilDeletion),
app_count: String(appIds.length),
},
}, { background: false })
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

return c.json(BRES)
})
2 changes: 2 additions & 0 deletions supabase/functions/triggers/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { app as canceled_org_retention_alerts } from '../_backend/triggers/canceled_org_retention_alerts.ts'
import { app as credit_usage_alerts } from '../_backend/triggers/credit_usage_alerts.ts'
import { app as credit_usage_posthog } from '../_backend/triggers/credit_usage_posthog.ts'
import { app as cron_clean_orphan_images } from '../_backend/triggers/cron_clean_orphan_images.ts'
Expand Down Expand Up @@ -77,6 +78,7 @@ appGlobal.route('/cron_clear_versions', cron_clear_versions)
appGlobal.route('/cron_clean_orphan_images', cron_clean_orphan_images)
appGlobal.route('/cron_reconcile_build_status', cron_reconcile_build_status)
appGlobal.route('/cron_rollout_auto_pause', cron_rollout_auto_pause)
appGlobal.route('/canceled_org_retention_alerts', canceled_org_retention_alerts)
appGlobal.route('/credit_usage_alerts', credit_usage_alerts)
appGlobal.route('/credit_usage_posthog', credit_usage_posthog)
appGlobal.route('/on_organization_delete', on_organization_delete)
Expand Down
Loading
Loading