Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ app.delete('/', middlewareKey(['all', 'write', 'upload']), async (c) => {
channel: 'upload-failed',
event: 'Failed to upload a bundle',
user_id: version.owner_org,
groups: { organization: version.owner_org },
icon: '💀',
})

Expand Down
22 changes: 16 additions & 6 deletions supabase/functions/_backend/private/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,26 @@ export const app = new Hono<MiddlewareKeyVariables>()

app.use('/', useCors)

interface ResolvedTrackingId {
trackingUserId: string
// Only set when we've verified the id refers to an organization the caller
// can access. Events for a bare authenticated user (no requestedUserId, or
// requestedUserId === authUserId) leave this undefined so we don't pollute
// the PostHog `organization` group with user UUIDs.
orgId?: string
}

async function resolveTrackingUserId(
c: Context<MiddlewareKeyVariables>,
requestedUserId: string | undefined,
appId: string | undefined,
notifyConsole = false,
) {
): Promise<ResolvedTrackingId> {
const forbiddenError = notifyConsole ? 'Forbidden' : 'no_permission'
const authUserId = c.get('auth')?.userId ?? ''

if (!requestedUserId || requestedUserId === authUserId) {
return authUserId
return { trackingUserId: authUserId }
}

if (appId) {
Expand All @@ -44,11 +53,11 @@ async function resolveTrackingUserId(
throw quickError(403, forbiddenError, 'You cannot send events for this organization')
}

return requestedUserId
return { trackingUserId: requestedUserId, orgId: requestedUserId }
}

if (await checkPermission(c, 'org.read', { orgId: requestedUserId })) {
return requestedUserId
return { trackingUserId: requestedUserId, orgId: requestedUserId }
}

throw quickError(403, forbiddenError, 'You cannot send events for this organization')
Expand Down Expand Up @@ -83,7 +92,7 @@ app.post('/', middlewareV2(['read', 'write', 'all', 'upload']), async (c) => {
: typeof body.tags?.app_id === 'string'
? body.tags.app_id
: undefined
const trackingUserId = await resolveTrackingUserId(c, requestedUserId, appId, Boolean(body.notifyConsole))
const { trackingUserId, orgId: verifiedOrgId } = await resolveTrackingUserId(c, requestedUserId, appId, Boolean(body.notifyConsole))
const trackedBody = requestedUserId ? { ...trackOptions, user_id: trackingUserId } : trackOptions

// notifyConsole: broadcast to Supabase Realtime only, skip all tracking
Expand All @@ -99,7 +108,7 @@ app.post('/', middlewareV2(['read', 'write', 'all', 'upload']), async (c) => {
description: trackedBody.description,
icon: trackedBody.icon,
app_id: appId,
org_id: trackingUserId,
org_id: requestedOrgId,
channel_name: typeof trackedBody.tags?.channel === 'string' ? trackedBody.tags.channel : undefined,
bundle_name: typeof trackedBody.tags?.bundle === 'string' ? trackedBody.tags.bundle : undefined,
timestamp: new Date().toISOString(),
Expand Down Expand Up @@ -146,6 +155,7 @@ app.post('/', middlewareV2(['read', 'write', 'all', 'upload']), async (c) => {
...trackedBody,
bento: onboardingBentoEvent,
sentToBento: Boolean(onboardingBentoEvent),
groups: verifiedOrgId ? { organization: verifiedOrgId } : undefined,
})

return c.json(BRES)
Expand Down
1 change: 1 addition & 0 deletions supabase/functions/_backend/private/upload_link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ app.post('/', middlewareKey(['all', 'write', 'upload']), async (c) => {
event: 'Upload via single file',
icon: '🏛️',
user_id: app.owner_org,
groups: { organization: app.owner_org },
notify: false,
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ app.post('/', middlewareAPISecret, async (c) => {
event: `Credit usage ${threshold}%+`,
icon: '⚡️',
user_id: orgId,
groups: { organization: orgId },
notify: threshold >= 100,
tags: {
alert_cycle: alertCycle.toString(),
Expand Down
1 change: 1 addition & 0 deletions supabase/functions/_backend/triggers/on_app_create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ app.post('/', middlewareAPISecret, triggerValidator('apps', 'INSERT'), async (c)
icon: isDemo ? '🎮' : isPendingOnboarding ? '🧭' : '🎉',
sentToBento: Boolean(appCreatedBentoEvent),
user_id: ownerOrg,
groups: { organization: ownerOrg },
tags: {
app_id: record.app_id,
is_demo: isDemo ? 'true' : 'false',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ app.post('/', middlewareAPISecret, triggerValidator('deploy_history', 'INSERT'),
event: 'Bundle Deployed',
icon: '🚀',
user_id: version.owner_org,
groups: { organization: version.owner_org },
tags: {
app_id: record.app_id,
bundle_name: version.name,
Expand Down
16 changes: 16 additions & 0 deletions supabase/functions/_backend/triggers/on_organization_create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import type { Database } from '../utils/supabase.types.ts'
import { Hono } from 'hono/tiny'
import { BRES, middlewareAPISecret, simpleError, triggerValidator } from '../utils/hono.ts'
import { cloudlog } from '../utils/logging.ts'
import { groupIdentifyPosthog } from '../utils/posthog.ts'
import { createStripeCustomer, finalizePendingStripeCustomer } from '../utils/supabase.ts'
import { sendEventToTracking } from '../utils/tracking.ts'
import { backgroundTask } from '../utils/utils.ts'

export const app = new Hono<MiddlewareKeyVariables>()

Expand All @@ -24,6 +26,19 @@ app.post('/', middlewareAPISecret, triggerValidator('orgs', 'INSERT'), async (c)
await finalizePendingStripeCustomer(c, record)
}

await backgroundTask(c, groupIdentifyPosthog(c, {
groupType: 'organization',
groupKey: record.id,
properties: {
name: record.name,
management_email: record.management_email,
customer_id: record.customer_id,
created_by: record.created_by,
created_at: record.created_at,
website: record.website,
Comment thread
WcaleNieWolny marked this conversation as resolved.
},
}))

await sendEventToTracking(c, {
bento: {
cron: '* * * * *',
Expand All @@ -40,6 +55,7 @@ app.post('/', middlewareAPISecret, triggerValidator('orgs', 'INSERT'), async (c)
icon: '🎉',
sentToBento: true,
user_id: record.id,
groups: { organization: record.id },
notify: false,
})

Expand Down
1 change: 1 addition & 0 deletions supabase/functions/_backend/triggers/on_version_create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ app.post('/', middlewareAPISecret, triggerValidator('app_versions', 'INSERT'), a
event: 'Bundle Created',
icon: '🎉',
user_id: record.owner_org,
groups: { organization: record.owner_org },
tags: {
app_id: record.app_id,
bundle_name: record.name,
Expand Down
28 changes: 28 additions & 0 deletions supabase/functions/_backend/triggers/stripe_event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
import { cloudlog } from '../utils/logging.ts'
import { closeClient, getDrizzleClient, getPgClient } from '../utils/pg.ts'
import * as schema from '../utils/postgres_schema.ts'
import { groupIdentifyPosthog } from '../utils/posthog.ts'
import { ensureCustomerMetadata, getCreditCheckoutDetails, syncStripeCustomerCountry } from '../utils/stripe.ts'
import { customerToSegmentOrg, supabaseAdmin } from '../utils/supabase.ts'
import { sendEventToTracking } from '../utils/tracking.ts'
import { backgroundTask } from '../utils/utils.ts'

export const app = new Hono<MiddlewareKeyVariablesStripe>()

Expand Down Expand Up @@ -276,6 +278,7 @@
icon: '💳',
sentToBento: true,
user_id: org.id,
groups: { organization: org.id },
notify: false,
})
return c.json(BRES)
Expand All @@ -295,6 +298,7 @@
icon: '⚠️',
sentToBento: true,
user_id: org.id,
groups: { organization: org.id },
notify: false,
})
return c.json(BRES)
Expand Down Expand Up @@ -332,12 +336,13 @@
icon: '📄',
sentToBento: true,
user_id: org.id,
groups: { organization: org.id },
notify: false,
})
return c.json(BRES)
}

async function createdOrUpdated(

Check failure on line 345 in supabase/functions/_backend/triggers/stripe_event.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 17 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=Cap-go_capgo&issues=AZ2wbHNeIVzJHEOEM24K&open=AZ2wbHNeIVzJHEOEM24K&pullRequest=1925
c: Context,
stripeData: StripeData,
org: Org,
Expand Down Expand Up @@ -384,6 +389,7 @@
icon: '💰',
sentToBento: true,
user_id: org.id,
groups: { organization: org.id },
notify: true,
tags: {
plan_name: plan.name,
Expand Down Expand Up @@ -418,11 +424,23 @@
icon: '💰',
sentToBento: true,
user_id: org.id,
groups: { organization: org.id },
notify: isNewSubscription,
tags: {
plan_name: plan.name,
},
})

await backgroundTask(c, groupIdentifyPosthog(c, {
groupType: 'organization',
groupKey: org.id,
properties: {
plan_name: plan.name,
plan_status: status,
plan_type: isMonthly ? 'monthly' : 'yearly',
subscription_status_name: statusName,
},
}))
}
else {
const segment = await customerToSegmentOrg(c, org.id, stripeData.data.price_id)
Expand Down Expand Up @@ -458,8 +476,18 @@
icon: '⚠️',
sentToBento: true,
user_id: org.id,
groups: { organization: org.id },
notify: true,
})

await backgroundTask(c, groupIdentifyPosthog(c, {
groupType: 'organization',
groupKey: org.id,
properties: {
plan_status: 'canceled',
canceled_at: new Date().toISOString(),
},
Comment thread
WcaleNieWolny marked this conversation as resolved.
}))
}

async function getOrg(c: Context, stripeData: StripeData) {
Expand Down
5 changes: 5 additions & 0 deletions supabase/functions/_backend/utils/plans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ async function userAbovePlan(c: Context, org: {
event: `User need upgrade to ${bestPlanKey}`,
icon: '⚠️',
user_id: orgId,
groups: { organization: orgId },
notify: false,
}).catch()
}
Expand All @@ -344,6 +345,7 @@ async function userIsAtPlanUsage(c: Context, orgId: string, customerId: string |
event: 'User is at 90% of plan usage',
icon: '⚠️',
user_id: orgId,
groups: { organization: orgId },
notify: false,
}).catch()
}
Expand All @@ -357,6 +359,7 @@ async function userIsAtPlanUsage(c: Context, orgId: string, customerId: string |
event: 'User is at 70% of plan usage',
icon: '⚠️',
user_id: orgId,
groups: { organization: orgId },
notify: false,
}).catch()
}
Expand All @@ -369,6 +372,7 @@ async function userIsAtPlanUsage(c: Context, orgId: string, customerId: string |
event: 'User is at 50% of plan usage',
icon: '⚠️',
user_id: orgId,
groups: { organization: orgId },
notify: false,
}).catch()
}
Expand Down Expand Up @@ -455,6 +459,7 @@ export async function handleOrgNotificationsAndEvents(c: Context, org: any, orgI
event: 'User need onboarding',
icon: '🥲',
user_id: orgId,
groups: { organization: orgId },
notify: false,
}).catch()
}
Expand Down
76 changes: 76 additions & 0 deletions supabase/functions/_backend/utils/posthog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
const POSTHOG_CAPTURE_URL = 'https://eu.i.posthog.com/capture/'
const POSTHOG_EXCEPTION_URL = 'https://eu.i.posthog.com/i/v0/e/'

export type PostHogGroups = Record<string, string>

interface PostHogCapturePayload extends Pick<TrackOptions, 'event'>, Pick<TrackOptions, 'channel' | 'description'> {
distinct_id?: string
groups?: PostHogGroups
ip?: string
setPersonProperties?: boolean
tags?: Record<string, any>
Expand All @@ -28,11 +31,14 @@

const distinctId = payload.user_id || payload.distinct_id || 'anonymous'

const hasGroups = payload.groups && Object.keys(payload.groups).length > 0

const properties = {
...(payload.tags || {}),
channel: payload.channel,
description: payload.description,
...(payload.setPersonProperties === false ? {} : { $set: payload.tags }),
...(hasGroups ? { $groups: payload.groups } : {}),
}

const body = {
Expand Down Expand Up @@ -228,3 +234,73 @@
return false
}
}

export interface PostHogGroupIdentifyPayload {
groupType: string
groupKey: string
properties?: Record<string, unknown>
}

export async function groupIdentifyPosthog(c: Context, payload: PostHogGroupIdentifyPayload) {
const apiKey = getEnv(c, 'POSTHOG_API_KEY')
if (!apiKey || !existInEnv(c, 'POSTHOG_API_KEY')) {
cloudlog({ requestId: c.get('requestId'), message: 'PostHog not configured' })
return false
}

const host = getEnv(c, 'POSTHOG_API_HOST') || POSTHOG_CAPTURE_URL
const posthogUrl = host.endsWith('/capture/')
? host
: new URL('capture/', host.endsWith('/') ? host : `${host}/`).toString()

Check warning on line 254 in supabase/functions/_backend/utils/posthog.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=Cap-go_capgo&issues=AZ2wbHIYIVzJHEOEM24J&open=AZ2wbHIYIVzJHEOEM24J&pullRequest=1925

const body = {
api_key: apiKey,
event: '$groupidentify',
distinct_id: `$${payload.groupType}_${payload.groupKey}`,
properties: {
$group_type: payload.groupType,
$group_key: payload.groupKey,
$group_set: payload.properties ?? {},
},
timestamp: new Date().toISOString(),
}

try {
const res = await fetch(posthogUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
Comment thread
WcaleNieWolny marked this conversation as resolved.

if (!res.ok) {
const error = await res.text()
cloudlogErr({
requestId: c.get('requestId'),
message: 'PostHog $groupidentify error',
status: res.status,
error,
groupType: payload.groupType,
groupKey: payload.groupKey,
})
return false
}

cloudlog({
requestId: c.get('requestId'),
message: 'PostHog $groupidentify sent',
groupType: payload.groupType,
groupKey: payload.groupKey,
})
return true
}
catch (e) {
cloudlogErr({
requestId: c.get('requestId'),
message: 'PostHog $groupidentify fetch failed',
error: serializeError(e),
groupType: payload.groupType,
groupKey: payload.groupKey,
})
return false
}
}
Loading
Loading