• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / storage / 30108976470

24 Jul 2026 04:25PM UTC coverage: 80.245% (-0.1%) from 80.35%
30108976470

Pull #1226

github

web-flow
Merge 72ddad7c3 into e2912273d
Pull Request #1226: feat: add continuous profiling and client

5619 of 7536 branches covered (74.56%)

Branch coverage included in aggregate %.

558 of 612 new or added lines in 13 files covered. (91.18%)

6 existing lines in 5 files now uncovered.

10609 of 12687 relevant lines covered (83.62%)

431.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.12
/src/scripts/pprof-client.ts
1
import path from 'node:path'
2
import { parseArgs } from 'node:util'
3
import type {
4
  PprofArchivedProfile,
5
  PprofArchivedProfileList,
6
} from '@internal/monitoring/pprof/client-http'
7
import {
8
  downloadArchivedProfile,
9
  fetchArchivedProfiles,
10
  fetchPprofStream,
11
  triggerPprofCapture,
12
} from '@internal/monitoring/pprof/client-http'
13
import { writePprofCaptureToFile } from '@internal/monitoring/pprof/download'
14
import { generateFlameArtifacts, resolveFlameMdFormat } from '@internal/monitoring/pprof/flame'
15
import type { ProfileClass, ProfileKind } from '@internal/monitoring/pprof/store-key'
16

17
const USAGE = `Usage:
1✔
18
  npm run pprof -- capture <profile|heap> [--seconds N]
19
  npm run pprof -- capture heap-snapshot [--output FILE]
20
  npm run pprof -- list --class <auto|manual> [--kind <cpu|heap>] [--days-ago N | --date YYYY-MM-DD | --all-dates] [--limit N] [--cursor TOKEN] [--all-pages] [--download DIRECTORY] [--flame]`
21

22
type PprofCommand =
23
  | {
24
      name: 'capture'
25
      target: 'profile' | 'heap'
26
      seconds: number
27
    }
28
  | {
29
      name: 'capture'
30
      target: 'heap-snapshot'
31
      output?: string
32
    }
33
  | {
34
      name: 'list'
35
      class: ProfileClass
36
      kind?: ProfileKind
37
      date?: string
38
      limit?: number
39
      cursor?: string
40
      allPages: boolean
41
      downloadDirectory?: string
42
      generateFlame: boolean
43
    }
44

45
function parsePositiveInteger(value: string | undefined, name: string, maximum?: number) {
46
  if (!value || !/^\d+$/.test(value)) throw new Error(`${name} must be a positive integer`)
4!
47
  const parsed = Number.parseInt(value, 10)
4✔
48
  if (!Number.isSafeInteger(parsed) || parsed <= 0 || (maximum !== undefined && parsed > maximum)) {
4✔
49
    throw new Error(
1✔
50
      `${name} must be a positive integer${maximum ? ` no greater than ${maximum}` : ''}`
1!
51
    )
52
  }
53
  return parsed
3✔
54
}
55

56
function parseNonNegativeInteger(value: string, name: string) {
57
  if (!/^\d+$/.test(value)) throw new Error(`${name} must be a non-negative integer`)
1!
58
  const parsed = Number.parseInt(value, 10)
1✔
59
  if (!Number.isSafeInteger(parsed)) throw new Error(`${name} must be a non-negative integer`)
1!
60
  return parsed
1✔
61
}
62

63
function parseNonEmptyString(value: string | undefined, name: string) {
64
  if (!value?.trim()) throw new Error(`${name} must not be empty`)
4✔
65
  return value
3✔
66
}
67

68
function parseProfileDate(value: string) {
69
  if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) throw new Error('date must use YYYY-MM-DD')
4!
70
  const timestamp = Date.parse(`${value}T00:00:00.000Z`)
4✔
71
  if (!Number.isFinite(timestamp) || new Date(timestamp).toISOString().slice(0, 10) !== value) {
4✔
72
    throw new Error('date must use YYYY-MM-DD')
1✔
73
  }
74
  return value
3✔
75
}
76

77
function utcDateDaysAgo(now: Date, daysAgo: number) {
78
  const date = new Date(
3✔
79
    Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - daysAgo)
80
  )
81
  if (Number.isNaN(date.getTime())) throw new Error('days-ago is outside the supported date range')
3!
82
  const formatted = date.toISOString().slice(0, 10)
3✔
83
  if (!/^\d{4}-\d{2}-\d{2}$/.test(formatted)) {
3!
NEW
84
    throw new Error('days-ago is outside the supported date range')
×
85
  }
86
  return formatted
3✔
87
}
88

89
export function parsePprofCommand(args: string[], now = new Date()): PprofCommand {
25✔
90
  const [name, ...rest] = args
25✔
91

92
  if (name === 'capture') {
25✔
93
    const [target, ...captureArgs] = rest
10✔
94
    if (target !== 'profile' && target !== 'heap' && target !== 'heap-snapshot') {
10!
NEW
95
      throw new Error(USAGE)
×
96
    }
97

98
    if (target === 'heap-snapshot') {
10✔
99
      const { values, positionals } = parseArgs({
3✔
100
        args: captureArgs,
101
        allowPositionals: true,
102
        strict: true,
103
        options: { output: { type: 'string' } },
104
      })
105
      if (positionals.length > 0) throw new Error(USAGE)
3!
106
      return { name, target, output: values.output }
1✔
107
    }
108

109
    const { values, positionals } = parseArgs({
7✔
110
      args: captureArgs,
111
      allowPositionals: true,
112
      strict: true,
113
      options: { seconds: { type: 'string' } },
114
    })
115
    if (positionals.length > 0) throw new Error(USAGE)
7!
116
    return {
3✔
117
      name,
118
      target,
119
      seconds:
120
        values.seconds === undefined ? 30 : parsePositiveInteger(values.seconds, 'seconds', 300),
3✔
121
    }
122
  }
123

124
  if (name === 'list') {
15✔
125
    const { values, positionals } = parseArgs({
13✔
126
      args: rest,
127
      allowPositionals: true,
128
      strict: true,
129
      options: {
130
        class: { type: 'string' },
131
        kind: { type: 'string' },
132
        'days-ago': { type: 'string' },
133
        date: { type: 'string' },
134
        'all-dates': { type: 'boolean' },
135
        'all-pages': { type: 'boolean' },
136
        download: { type: 'string' },
137
        flame: { type: 'boolean' },
138
        limit: { type: 'string' },
139
        cursor: { type: 'string' },
140
      },
141
    })
142
    if (positionals.length > 0 || (values.class !== 'auto' && values.class !== 'manual')) {
13✔
143
      throw new Error('--class must be auto or manual')
1✔
144
    }
145
    if (values.kind !== undefined && values.kind !== 'cpu' && values.kind !== 'heap') {
11!
NEW
146
      throw new Error('--kind must be cpu or heap')
×
147
    }
148
    const dateSelectors = [values['days-ago'], values.date, values['all-dates'] === true].filter(
11✔
149
      (value) => value !== undefined && value !== false
33✔
150
    )
151
    if (dateSelectors.length > 1)
11✔
152
      throw new Error('--days-ago, --date and --all-dates are mutually exclusive')
1✔
153
    const daysAgo =
154
      values['days-ago'] === undefined ? 0 : parseNonNegativeInteger(values['days-ago'], 'days-ago')
10✔
155
    if (values.flame === true && values.download === undefined) {
13✔
156
      throw new Error('--flame requires --download')
1✔
157
    }
158
    return {
9✔
159
      name,
160
      class: values.class,
161
      kind: values.kind,
162
      date:
163
        values['all-dates'] === true
9✔
164
          ? undefined
165
          : values.date === undefined
7✔
166
            ? utcDateDaysAgo(now, daysAgo)
167
            : parseProfileDate(values.date),
168
      limit:
169
        values.limit === undefined ? undefined : parsePositiveInteger(values.limit, 'limit', 1000),
8✔
170
      cursor: values.cursor,
171
      allPages: values['all-pages'] === true,
172
      downloadDirectory:
173
        values.download === undefined
8✔
174
          ? undefined
175
          : parseNonEmptyString(values.download, 'download directory'),
176
      generateFlame: values.flame === true,
177
    }
178
  }
179

180
  throw new Error(USAGE)
2✔
181
}
182

183
async function generateFlame(profilePath: string, enabled: boolean) {
184
  if (!enabled) return
3!
NEW
185
  await generateFlameArtifacts(profilePath, {
×
186
    env: {
187
      ...process.env,
188
      FLAME_SOURCEMAPS_DIRS: process.env.FLAME_SOURCEMAPS_DIRS || 'dist',
×
189
    },
190
    mdFormat: resolveFlameMdFormat(process.env.PPROF_FLAME_MD_FORMAT),
191
  })
192
}
193

194
function bulkDownloadFilename(profile: PprofArchivedProfile) {
195
  const key = profile.key.match(/^v1\/(auto|manual)\/\d{13}-([a-f0-9]{12})\/(cpu|heap)\//)
3✔
196
  const startedAt = new Date(profile.startedAt)
3✔
197
  if (!key || Number.isNaN(startedAt.getTime())) {
3!
NEW
198
    throw new Error(`Invalid profile returned by list: ${profile.key}`)
×
199
  }
200
  const timestamp = startedAt.toISOString().replace(/[:.]/g, '-')
3✔
201
  return `${key[1]}-${key[3]}-${timestamp}-${key[2]}.pprof.gz`
3✔
202
}
203

204
async function fetchProfilePages(
205
  command: Extract<PprofCommand, { name: 'list' }>,
206
  adminUrl: string,
207
  apiKey: string
208
) {
209
  const profiles: PprofArchivedProfile[] = []
2✔
210
  const seenCursors = new Set<string>()
2✔
211
  let cursor = command.cursor
2✔
212
  if (cursor) seenCursors.add(cursor)
2!
213

214
  while (true) {
2✔
215
    const page = await fetchArchivedProfiles({
3✔
216
      adminUrl,
217
      apiKey,
218
      class: command.class,
219
      kind: command.kind,
220
      date: command.date,
221
      limit: command.limit,
222
      cursor,
223
    })
224
    profiles.push(...page.profiles)
3✔
225

226
    if (!command.allPages || page.cursor === undefined) {
3✔
227
      return {
2✔
228
        profiles,
229
        cursor: page.cursor,
230
      } satisfies PprofArchivedProfileList
231
    }
232
    if (seenCursors.has(page.cursor)) {
1!
NEW
233
      throw new Error('Pprof list returned a repeated cursor')
×
234
    }
235
    seenCursors.add(page.cursor)
1✔
236
    cursor = page.cursor
1✔
237
  }
238
}
239

240
async function downloadProfiles(
241
  profiles: PprofArchivedProfile[],
242
  directory: string,
243
  adminUrl: string,
244
  apiKey: string,
245
  generateFlameFiles: boolean
246
) {
247
  for (const profile of profiles) {
2✔
248
    const response = await downloadArchivedProfile({ adminUrl, apiKey, key: profile.key })
3✔
249
    const { outputPath } = await writePprofCaptureToFile(
3✔
250
      response.stream,
251
      {
252
        contentDisposition: response.contentDisposition,
253
        type: 'profile',
254
      },
255
      {
256
        outputPath: path.join(directory, bulkDownloadFilename(profile)),
257
      }
258
    )
259
    await generateFlame(outputPath, generateFlameFiles)
3✔
260
  }
261
}
262

263
export async function executePprofCommand(command: PprofCommand, adminUrl: string, apiKey: string) {
264
  if (command.name === 'list') {
2!
265
    const result = await fetchProfilePages(command, adminUrl, apiKey)
2✔
266
    console.log(JSON.stringify(result, null, 2))
2✔
267
    if (command.downloadDirectory) {
2!
268
      await downloadProfiles(
2✔
269
        result.profiles,
270
        command.downloadDirectory,
271
        adminUrl,
272
        apiKey,
273
        command.generateFlame
274
      )
275
    }
276
    return result
2✔
277
  }
278

NEW
279
  if (command.target !== 'heap-snapshot') {
×
NEW
280
    console.log(
×
281
      JSON.stringify(
282
        await triggerPprofCapture({
283
          adminUrl,
284
          apiKey,
285
          type: command.target === 'profile' ? 'cpu' : 'heap',
×
286
          seconds: command.seconds,
287
        }),
288
        null,
289
        2
290
      )
291
    )
UNCOV
292
    return
×
293
  }
294

NEW
295
  const response = await fetchPprofStream({
×
296
    adminUrl,
297
    apiKey,
298
    type: command.target,
299
  })
NEW
300
  await writePprofCaptureToFile(
×
301
    response.stream,
302
    {
303
      contentDisposition: response.contentDisposition,
304
      type: command.target,
305
    },
306
    { outputPath: command.output }
307
  )
308
}
309

310
async function main() {
NEW
311
  const adminUrl = process.env.ADMIN_URL
×
NEW
312
  const apiKey = process.env.ADMIN_API_KEY
×
NEW
313
  if (!adminUrl) throw new Error('Please provide ADMIN_URL')
×
NEW
314
  if (!apiKey) throw new Error('Please provide ADMIN_API_KEY')
×
NEW
315
  await executePprofCommand(parsePprofCommand(process.argv.slice(2)), adminUrl, apiKey)
×
316
}
317

318
if (require.main === module) {
1!
UNCOV
319
  main().catch((error) => {
×
320
    process.exitCode = 1
×
NEW
321
    console.error(error instanceof Error ? error.message : error)
×
322
  })
323
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc