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
12 changes: 8 additions & 4 deletions .github/workflows/bump_version.yml
Original file line number Diff line number Diff line change
Expand Up @@ -236,11 +236,15 @@ jobs:
# Gate the auto-sync on its own output: the regenerated types are only
# trustworthy if they still compile against the backend that consumes
# them. Prod is often behind local migrations, so a regenerated types
# file can drop columns the code still references. Fail the job here
# instead of pushing an unverified commit that reddens `main` for the
# next contributor.
# file can drop columns the code still references. Skip the push in
# that case instead of landing an unverified commit that reddens
# `main` — and exit 0 so migration-touching releases are not blocked.
echo "Schema/types drift detected; verifying regenerated types compile against the codebase."
bun typecheck
if ! bun typecheck; then
echo "Regenerated schema/types do not compile (prod likely behind local migrations). Skipping auto-sync push." >&2
git checkout -- supabase/schemas/prod.sql src/types/supabase.types.ts supabase/functions/_backend/utils/supabase.types.ts
exit 0
fi

git config --local user.name "github-actions[bot]"
git config --local user.email "github-actions[bot]@users.noreply.github.com"
Expand Down
58 changes: 48 additions & 10 deletions supabase/functions/_backend/triggers/logsnag_insights.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,8 +327,15 @@ type RequiredGlobalStatsShard = typeof REQUIRED_GLOBAL_STATS_SHARDS[number]
type GlobalStatsNotificationStepAction = 'send' | 'complete_claimed' | 'skip'
type GlobalStatsUpdate = Database['public']['Tables']['global_stats']['Update']
type GlobalStatsRow = Database['public']['Tables']['global_stats']['Row']
type GlobalStatsSnapshotPatch = GlobalStatsUpdate & { orgs?: number }
type GlobalStatsSnapshotRow = GlobalStatsRow & { orgs?: number | null }
type GlobalStatsSnapshotPatch = GlobalStatsUpdate & {
orgs?: number
// Present in repo migrations before prod deploy; keep writable while generated types lag.
apps_with_preview?: number
}
type GlobalStatsSnapshotRow = GlobalStatsRow & {
orgs?: number | null
apps_with_preview?: number | null
}

interface LogsnagInsightsPayload {
retry_count?: unknown
Expand Down Expand Up @@ -790,14 +797,24 @@ function shouldRefreshMutablePastDueStats(
return !hasPersistedPastDueStats(snapshot)
}

function isMissingBuildMetricColumnError(error: unknown): boolean {
function isMissingSchemaColumnError(error: unknown, columnHints: string[]): boolean {
const errorCode = String((error as any)?.code ?? '').toUpperCase()
const message = String((error as any)?.message ?? '').toLowerCase()
return errorCode === 'PGRST204'
|| errorCode === '42703'
|| message.includes('build_total_seconds_day')
|| message.includes('build_avg_seconds_day')
|| message.includes('build_count_day')
|| columnHints.some(hint => message.includes(hint.toLowerCase()))
}

function isMissingBuildMetricColumnError(error: unknown): boolean {
return isMissingSchemaColumnError(error, [
'build_total_seconds_day',
'build_avg_seconds_day',
'build_count_day',
])
}

function isMissingAppsWithPreviewColumnError(error: unknown): boolean {
return isMissingSchemaColumnError(error, ['apps_with_preview'])
}

async function calculateRevenue(c: Context, referenceDate?: Date): Promise<PlanRevenue> {
Expand Down Expand Up @@ -1592,14 +1609,35 @@ async function ensureGlobalStatsSnapshotRows(c: Context, dateIds: readonly strin
async function updateGlobalStatsSnapshot(c: Context, dateId: string, patch: GlobalStatsSnapshotPatch): Promise<void> {
await ensureGlobalStatsSnapshotRow(c, dateId)

const { orgs, ...globalStatsPatch } = patch
const { orgs, apps_with_preview, ...globalStatsPatch } = patch
const updatePayload = {
...globalStatsPatch,
...(apps_with_preview === undefined ? {} : { apps_with_preview }),
} as GlobalStatsUpdate
const { error } = await supabaseAdmin(c)
.from('global_stats')
.update(globalStatsPatch as GlobalStatsUpdate)
.update(updatePayload)
.eq('date_id', dateId)

if (error)
throw error
if (error) {
if (apps_with_preview !== undefined && isMissingAppsWithPreviewColumnError(error)) {
cloudlog({
requestId: c.get('requestId'),
message: 'global_stats.apps_with_preview missing; retrying snapshot update without it',
dateId,
error,
})
const { error: legacyError } = await supabaseAdmin(c)
.from('global_stats')
.update(globalStatsPatch as GlobalStatsUpdate)
.eq('date_id', dateId)
if (legacyError)
throw legacyError
}
else {
throw error
}
}

if (orgs !== undefined)
await updateGlobalStatsSnapshotOrgCount(c, dateId, orgs)
Expand Down
3 changes: 3 additions & 0 deletions tests/logsnag-insights-revenue.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,9 @@ describe('logsnag revenue metric helpers', () => {
expect(countFn).toContain('snapshotEnd')
expect(coreShard).toContain('countAppsWithPreview(c, window.prevDayEnd)')
expect(coreShard).toContain('apps_with_preview,')
// Keep writable while prod types lag the migration (auto-sync gate).
expect(source).toContain('apps_with_preview?: number')
expect(source).toContain('isMissingAppsWithPreviewColumnError')
})
it.concurrent('normalizes logsnag insights retry payload counts', () => {
expect(logsnagInsightsTestUtils.normalizeLogsnagInsightsRetryCount('2')).toBe(2)
Expand Down
Loading