github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/webapp/javascript/services/render.ts (about) 1 import { Result } from '@webapp/util/fp'; 2 import { 3 Profile, 4 Groups, 5 FlamebearerProfileSchema, 6 GroupsSchema, 7 } from '@pyroscope/models/src'; 8 import { z } from 'zod'; 9 import type { ZodError } from 'zod'; 10 import { 11 buildRenderURL, 12 buildMergeURLWithQueryID, 13 } from '@webapp/util/updateRequests'; 14 import { Timeline, TimelineSchema } from '@webapp/models/timeline'; 15 import { Annotation, AnnotationSchema } from '@webapp/models/annotation'; 16 import type { RequestError } from './base'; 17 import { request, parseResponse } from './base'; 18 19 export interface RenderOutput { 20 profile: Profile; 21 timeline: Timeline; 22 groups?: Groups; 23 annotations: Annotation[]; 24 } 25 26 // Default to empty array if not present 27 const defaultAnnotationsSchema = z.preprocess((a) => { 28 if (!a) { 29 return []; 30 } 31 return a; 32 }, z.array(AnnotationSchema)); 33 34 interface renderSingleProps { 35 from: string; 36 until: string; 37 query: string; 38 refreshToken?: string; 39 maxNodes: string | number; 40 } 41 export async function renderSingle( 42 props: renderSingleProps, 43 controller?: { 44 signal?: AbortSignal; 45 } 46 ): Promise<Result<RenderOutput, RequestError | ZodError>> { 47 const url = buildRenderURL(props); 48 // TODO 49 const response = await request(`${url}&format=json`, { 50 signal: controller?.signal, 51 }); 52 53 if (response.isErr) { 54 return Result.err<RenderOutput, RequestError>(response.error); 55 } 56 57 const parsed = FlamebearerProfileSchema.merge( 58 z.object({ 59 timeline: TimelineSchema, 60 annotations: defaultAnnotationsSchema, 61 }) 62 ) 63 .merge(z.object({ telemetry: z.object({}).passthrough().optional() })) 64 .safeParse(response.value); 65 66 if (parsed.success) { 67 // TODO: strip timeline 68 const profile = parsed.data; 69 const { timeline, annotations } = parsed.data; 70 71 return Result.ok({ 72 profile, 73 timeline, 74 annotations, 75 }); 76 } 77 78 return Result.err(parsed.error); 79 } 80 81 interface mergeWithQueryIDProps { 82 queryID: string; 83 refreshToken?: string; 84 maxNodes: string | number; 85 } 86 87 interface MergeMetadata { 88 appName: string; 89 startTime: string; 90 endTime: string; 91 profilesLength: number; 92 } 93 94 const MergeMetadataSchema = z.object({ 95 appName: z.string(), 96 startTime: z.string(), 97 endTime: z.string(), 98 profilesLength: z.number(), 99 }); 100 101 export interface MergeOutput { 102 profile: Profile; 103 mergeMetadata: MergeMetadata; 104 } 105 106 export async function mergeWithQueryID( 107 props: mergeWithQueryIDProps, 108 controller?: { 109 signal?: AbortSignal; 110 } 111 ): Promise<Result<MergeOutput, RequestError | ZodError>> { 112 const url = buildMergeURLWithQueryID(props); 113 // TODO 114 const response = await request(`${url}&format=json`, { 115 signal: controller?.signal, 116 }); 117 118 if (response.isErr) { 119 return Result.err<MergeOutput, RequestError>(response.error); 120 } 121 122 const parsed = FlamebearerProfileSchema.merge( 123 z.object({ timeline: TimelineSchema }) 124 ) 125 .merge(z.object({ mergeMetadata: MergeMetadataSchema })) 126 .merge(z.object({ telemetry: z.object({}).passthrough().optional() })) 127 .safeParse(response.value); 128 129 if (parsed.success) { 130 // TODO: strip timeline 131 const profile = parsed.data; 132 const { mergeMetadata } = parsed.data; 133 134 return Result.ok({ 135 profile, 136 mergeMetadata, 137 }); 138 } 139 140 return Result.err(parsed.error); 141 } 142 143 const HeatmapSchema = z.object({ 144 startTime: z.number(), 145 endTime: z.number(), 146 minValue: z.number(), 147 maxValue: z.number(), 148 minDepth: z.number(), 149 maxDepth: z.number(), 150 timeBuckets: z.number(), 151 valueBuckets: z.number(), 152 values: z.array(z.array(z.number())), 153 }); 154 155 export interface getHeatmapProps { 156 query: string; 157 from: string; 158 until: string; 159 minValue: number; 160 maxValue: number; 161 heatmapTimeBuckets: number; 162 heatmapValueBuckets: number; 163 maxNodes?: string; 164 } 165 166 export type Heatmap = z.infer<typeof HeatmapSchema>; 167 export interface HeatmapOutput { 168 heatmap: Heatmap | null; 169 profile?: Profile; 170 } 171 172 export async function getHeatmap( 173 props: getHeatmapProps, 174 controller?: { 175 signal?: AbortSignal; 176 } 177 ): Promise<Result<HeatmapOutput, RequestError | ZodError>> { 178 const params = new URLSearchParams({ 179 ...props, 180 minValue: props.minValue.toString(), 181 maxValue: props.maxValue.toString(), 182 heatmapTimeBuckets: props.heatmapTimeBuckets.toString(), 183 heatmapValueBuckets: props.heatmapValueBuckets.toString(), 184 }); 185 186 const response = await request(`/api/exemplars:query?${params}`, { 187 signal: controller?.signal, 188 }); 189 190 if (response.isOk) { 191 const parsed = FlamebearerProfileSchema.merge( 192 z.object({ timeline: TimelineSchema }) 193 ) 194 .merge(z.object({ telemetry: z.object({}).passthrough().optional() })) 195 .merge(z.object({ heatmap: HeatmapSchema.nullable() })) 196 .safeParse(response.value); 197 198 if (parsed.success) { 199 const profile = parsed.data; 200 const { heatmap } = parsed.data; 201 202 if (heatmap !== null) { 203 return Result.ok({ 204 heatmap, 205 profile, 206 }); 207 } 208 209 return Result.ok({ 210 heatmap: null, 211 }); 212 } 213 214 return Result.err<HeatmapOutput, RequestError>(response.error); 215 } 216 217 return Result.err<HeatmapOutput, RequestError>(response.error); 218 } 219 220 export interface SelectionProfileOutput { 221 selectionProfile: Profile; 222 } 223 224 export interface selectionProfileProps { 225 from: string; 226 until: string; 227 query: string; 228 selectionStartTime: number; 229 selectionEndTime: number; 230 selectionMinValue: number; 231 selectionMaxValue: number; 232 heatmapTimeBuckets: number; 233 heatmapValueBuckets: number; 234 } 235 236 export async function getHeatmapSelectionProfile( 237 props: selectionProfileProps, 238 controller?: { 239 signal?: AbortSignal; 240 } 241 ): Promise<Result<SelectionProfileOutput, RequestError | ZodError>> { 242 const params = new URLSearchParams({ 243 ...props, 244 selectionStartTime: props.selectionStartTime.toString(), 245 selectionEndTime: props.selectionEndTime.toString(), 246 selectionMinValue: props.selectionMinValue.toString(), 247 selectionMaxValue: props.selectionMaxValue.toString(), 248 heatmapTimeBuckets: props.heatmapTimeBuckets.toString(), 249 heatmapValueBuckets: props.heatmapValueBuckets.toString(), 250 }); 251 252 const response = await request(`/api/exemplars:query?${params}`, { 253 signal: controller?.signal, 254 }); 255 256 if (response.isOk) { 257 const parsed = FlamebearerProfileSchema.merge( 258 z.object({ timeline: TimelineSchema }) 259 ) 260 .merge(z.object({ telemetry: z.object({}).passthrough().optional() })) 261 .safeParse(response.value); 262 263 if (parsed.success) { 264 return Result.ok({ 265 selectionProfile: parsed.data, 266 }); 267 } 268 269 return Result.err<SelectionProfileOutput, RequestError>(response.error); 270 } 271 272 return Result.err<SelectionProfileOutput, RequestError>(response.error); 273 } 274 275 export type RenderDiffResponse = z.infer<typeof FlamebearerProfileSchema>; 276 277 interface renderDiffProps { 278 leftFrom: string; 279 leftUntil: string; 280 leftQuery: string; 281 refreshToken?: string; 282 maxNodes: string; 283 rightQuery: string; 284 rightFrom: string; 285 rightUntil: string; 286 } 287 export async function renderDiff( 288 props: renderDiffProps, 289 controller?: { 290 signal?: AbortSignal; 291 } 292 ) { 293 const params = new URLSearchParams({ 294 leftQuery: props.leftQuery, 295 leftFrom: props.leftFrom, 296 leftUntil: props.leftUntil, 297 rightQuery: props.rightQuery, 298 rightFrom: props.rightFrom, 299 rightUntil: props.rightUntil, 300 format: 'json', 301 }); 302 303 const response = await request(`/render-diff?${params}`, { 304 signal: controller?.signal, 305 }); 306 307 return parseResponse<z.infer<typeof FlamebearerProfileSchema>>( 308 response, 309 FlamebearerProfileSchema 310 ); 311 } 312 313 interface renderExploreProps extends Omit<renderSingleProps, 'maxNodes'> { 314 groupBy: string; 315 grouByTagValue: string; 316 } 317 318 export interface RenderExploreOutput { 319 profile: Profile; 320 groups: Groups; 321 } 322 323 export async function renderExplore( 324 props: renderExploreProps, 325 controller?: { 326 signal?: AbortSignal; 327 } 328 ): Promise<Result<RenderExploreOutput, RequestError | ZodError>> { 329 const url = buildRenderURL(props); 330 331 const response = await request(`${url}&format=json`, { 332 signal: controller?.signal, 333 }); 334 335 if (response.isErr) { 336 return Result.err<RenderExploreOutput, RequestError>(response.error); 337 } 338 339 const parsed = FlamebearerProfileSchema.merge( 340 z.object({ timeline: TimelineSchema }) 341 ) 342 .merge( 343 z.object({ 344 telemetry: z.object({}).passthrough().optional(), 345 annotations: defaultAnnotationsSchema, 346 }) 347 ) 348 .merge( 349 z.object({ 350 groups: z.preprocess((groups) => { 351 const groupNames = Object.keys(groups as Groups); 352 353 return groupNames.length 354 ? groupNames 355 .filter((g) => !!g.trim()) 356 .reduce( 357 (acc, current) => ({ 358 ...acc, 359 [current]: (groups as Groups)[current], 360 }), 361 {} 362 ) 363 : groups; 364 }, GroupsSchema), 365 }) 366 ) 367 .safeParse(response.value); 368 369 if (parsed.success) { 370 const profile = parsed.data; 371 const { groups, annotations } = parsed.data; 372 373 return Result.ok({ 374 profile, 375 groups, 376 annotations, 377 }); 378 } 379 380 return Result.err(parsed.error); 381 }