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  }