Skip to content

Commit

Permalink
ref(crons): new mergeBuckets timeline function without env (#82569)
Browse files Browse the repository at this point in the history
new `mergeBuckets` function that merges new `monitorBucketWithStats`
type

since `checkinTimeline` doesn't need to know anything about environment,
we want to replace `MonitorBucketEnvMapping` fields with `StatsBucket`
  • Loading branch information
ameliahsu authored Jan 6, 2025
1 parent 7973237 commit 54be212
Show file tree
Hide file tree
Showing 5 changed files with 242 additions and 10 deletions.
10 changes: 10 additions & 0 deletions static/app/views/monitors/components/timeline/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export interface TimeWindowConfig {
}

export type MonitorBucket = [timestamp: number, envData: MonitorBucketEnvMapping];
export type MonitorBucketWithStats = [timestamp: number, stats: StatsBucket];

export interface JobTickData {
endTs: number;
Expand All @@ -62,6 +63,15 @@ export interface JobTickData {
width: number;
}

export interface JobTickDataWithStats {
endTs: number;
roundedLeft: boolean;
roundedRight: boolean;
startTs: number;
stats: StatsBucket;
width: number;
}

export type StatsBucket = {
[CheckInStatus.IN_PROGRESS]: number;
[CheckInStatus.OK]: number;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {isEnvMappingEmpty} from './isEnvMappingEmpty';
import {isEnvMappingEmpty, isStatsBucketEmpty} from './isEnvMappingEmpty';

describe('isEnvMappingEmpty', function () {
it('returns true for an empty env', function () {
Expand All @@ -13,3 +13,15 @@ describe('isEnvMappingEmpty', function () {
expect(isEnvMappingEmpty(envMapping)).toBe(false);
});
});

describe('isStatsBucketEmpty', function () {
it('returns true for an empty env', function () {
const stats = {ok: 0, missed: 0, timeout: 0, error: 0, in_progress: 0, unknown: 0};
expect(isStatsBucketEmpty(stats)).toEqual(true);
});

it('returns false for a filled env', function () {
const stats = {ok: 1, missed: 0, timeout: 0, error: 0, in_progress: 0, unknown: 0};
expect(isStatsBucketEmpty(stats)).toEqual(false);
});
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import type {MonitorBucketEnvMapping} from '../types';
import type {MonitorBucketEnvMapping, StatsBucket} from '../types';

/**
* Determines if an environment mapping includes any job run data
*/
export function isEnvMappingEmpty(envMapping: MonitorBucketEnvMapping) {
return Object.keys(envMapping).length === 0;
}

export function isStatsBucketEmpty(stats: StatsBucket): boolean {
return Object.values(stats).every(value => value === 0);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {CheckInStatus} from 'sentry/views/monitors/types';

import type {MonitorBucket} from '../types';
import type {MonitorBucket, MonitorBucketWithStats} from '../types';

import {mergeBuckets} from './mergeBuckets';
import {mergeBuckets, mergeBucketsWithStats} from './mergeBuckets';

type StatusCounts = [
in_progress: number,
Expand Down Expand Up @@ -34,6 +34,32 @@ function generateJobRun(envName: string, jobStatus: CheckInStatus) {
return generateEnvMapping(envName, counts);
}

export function generateStats(counts: StatusCounts) {

Check failure on line 37 in static/app/views/monitors/components/timeline/utils/mergeBuckets.spec.tsx

View workflow job for this annotation

GitHub Actions / pre-commit lint

Do not export from a test file
const [in_progress, ok, missed, timeout, error, unknown] = counts;
return {
in_progress,
ok,
missed,
timeout,
error,
unknown,
};
}

function generateJobRunWithStats(jobStatus: CheckInStatus) {
const sortedStatuses = [
CheckInStatus.IN_PROGRESS,
CheckInStatus.OK,
CheckInStatus.MISSED,
CheckInStatus.TIMEOUT,
CheckInStatus.ERROR,
CheckInStatus.UNKNOWN,
];
const counts: StatusCounts = [0, 0, 0, 0, 0, 0];
counts[sortedStatuses.indexOf(jobStatus)] = 1;
return generateStats(counts);
}

describe('mergeBuckets', function () {
it('does not generate ticks less than 3px width', function () {
const bucketData: MonitorBucket[] = [
Expand Down Expand Up @@ -166,3 +192,91 @@ describe('mergeBuckets', function () {
expect(mergedData).toEqual(expectedMerged);
});
});

describe('mergeBucketsWithStats', function () {
it('does not generate ticks less than 3px width', function () {
const bucketData: MonitorBucketWithStats[] = [
[1, generateJobRunWithStats(CheckInStatus.OK)],
[2, generateJobRunWithStats(CheckInStatus.OK)],
[3, generateJobRunWithStats(CheckInStatus.OK)],
[4, generateStats([0, 0, 0, 0, 0, 0])],
[5, generateJobRunWithStats(CheckInStatus.OK)],
[6, generateJobRunWithStats(CheckInStatus.OK)],
[7, generateJobRunWithStats(CheckInStatus.OK)],
[8, generateJobRunWithStats(CheckInStatus.OK)],
];
const mergedData = mergeBucketsWithStats(bucketData);
const expectedMerged = [
{
startTs: 1,
endTs: 8,
width: 8,
roundedLeft: true,
roundedRight: true,
stats: generateStats([0, 7, 0, 0, 0, 0]),
},
];

expect(mergedData).toEqual(expectedMerged);
});

it('generates adjacent ticks without border radius', function () {
const bucketData: MonitorBucketWithStats[] = [
[1, generateJobRunWithStats(CheckInStatus.OK)],
[2, generateJobRunWithStats(CheckInStatus.OK)],
[3, generateJobRunWithStats(CheckInStatus.OK)],
[4, generateJobRunWithStats(CheckInStatus.OK)],
[5, generateJobRunWithStats(CheckInStatus.MISSED)],
[6, generateJobRunWithStats(CheckInStatus.TIMEOUT)],
[7, generateJobRunWithStats(CheckInStatus.MISSED)],
[8, generateJobRunWithStats(CheckInStatus.MISSED)],
];
const mergedData = mergeBucketsWithStats(bucketData);
const expectedMerged = [
{
startTs: 1,
endTs: 4,
width: 4,
roundedLeft: true,
roundedRight: false,
stats: generateStats([0, 4, 0, 0, 0, 0]),
},
{
startTs: 5,
endTs: 8,
width: 4,
roundedLeft: false,
roundedRight: true,
stats: generateStats([0, 0, 3, 1, 0, 0]),
},
];

expect(mergedData).toEqual(expectedMerged);
});

it('does not generate a separate tick if the next generated tick would be the same status', function () {
const bucketData: MonitorBucketWithStats[] = [
[1, generateJobRunWithStats(CheckInStatus.TIMEOUT)],
[2, generateJobRunWithStats(CheckInStatus.TIMEOUT)],
[3, generateJobRunWithStats(CheckInStatus.TIMEOUT)],
[4, generateJobRunWithStats(CheckInStatus.TIMEOUT)],
[5, generateJobRunWithStats(CheckInStatus.MISSED)],
[6, generateJobRunWithStats(CheckInStatus.OK)],
[7, generateJobRunWithStats(CheckInStatus.MISSED)],
[8, generateJobRunWithStats(CheckInStatus.TIMEOUT)],
];
const mergedData = mergeBucketsWithStats(bucketData);
const expectedMerged = [
{
startTs: 1,
endTs: 8,
width: 8,
roundedLeft: true,
roundedRight: true,
stats: generateStats([0, 1, 2, 5, 0, 0]),
},
];

expect(mergedData).toEqual(expectedMerged);
});
});
104 changes: 98 additions & 6 deletions static/app/views/monitors/components/timeline/utils/mergeBuckets.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
import type {JobTickData, MonitorBucket} from '../types';
import {filterMonitorStatsBucketByEnv} from 'sentry/views/monitors/components/timeline/utils/filterMonitorStatsBucketByEnv';

import {filterMonitorStatsBucketByEnv} from './filterMonitorStatsBucketByEnv';
import {getAggregateStatus} from './getAggregateStatus';
import {getAggregateStatusFromMultipleBuckets} from './getAggregateStatusFromMultipleBuckets';
import {isEnvMappingEmpty} from './isEnvMappingEmpty';
import {mergeEnvMappings} from './mergeEnvMappings';
import type {
JobTickData,
JobTickDataWithStats,
MonitorBucket,
MonitorBucketWithStats,
} from '../types';

import {
getAggregateStatus,
getAggregateStatusFromStatsBucket,
} from './getAggregateStatus';
import {
getAggregateStatusFromMultipleBuckets,
getAggregateStatusFromMultipleStatsBuckets,
} from './getAggregateStatusFromMultipleBuckets';
import {isEnvMappingEmpty, isStatsBucketEmpty} from './isEnvMappingEmpty';
import {mergeEnvMappings, mergeStats} from './mergeEnvMappings';

function generateJobTickFromBucket(
bucket: MonitorBucket,
Expand All @@ -22,6 +34,22 @@ function generateJobTickFromBucket(
};
}

function generateJobTickFromBucketWithStats(
bucket: MonitorBucketWithStats,
options?: Partial<JobTickDataWithStats>
) {
const [timestamp, stats] = bucket;
return {
endTs: timestamp,
startTs: timestamp,
width: 1,
stats,
roundedLeft: false,
roundedRight: false,
...options,
};
}

export function mergeBuckets(data: MonitorBucket[], environment: string) {
const minTickWidth = 4;

Expand Down Expand Up @@ -82,3 +110,67 @@ export function mergeBuckets(data: MonitorBucket[], environment: string) {

return jobTicks;
}

export function mergeBucketsWithStats(
data: MonitorBucketWithStats[]
): JobTickDataWithStats[] {
const minTickWidth = 4;
const jobTicks: JobTickDataWithStats[] = [];

data.reduce(
(currentJobTick: JobTickDataWithStats | null, [timestamp, stats], i) => {
const statsEmpty = isStatsBucketEmpty(stats);

// If no current job tick, we start the first one
if (!currentJobTick) {
return statsEmpty
? currentJobTick
: generateJobTickFromBucketWithStats([timestamp, stats], {roundedLeft: true});
}

const bucketStatus = getAggregateStatusFromStatsBucket(stats);
const currJobTickStatus = getAggregateStatusFromStatsBucket(currentJobTick.stats);

// If the current stats are empty and our job tick has reached the min width, finalize the tick
if (statsEmpty && currentJobTick.width >= minTickWidth) {
currentJobTick.roundedRight = true;
jobTicks.push(currentJobTick);
return null;
}

// Calculate the aggregate status for the next minTickWidth buckets
const nextTickAggregateStatus = getAggregateStatusFromMultipleStatsBuckets(
data.slice(i, i + minTickWidth).map(([_, sliceStats]) => sliceStats)
);

// If the status changes or we reach the min width, push the current tick and start a new one
if (
bucketStatus !== currJobTickStatus &&
nextTickAggregateStatus !== currJobTickStatus &&
currentJobTick.width >= minTickWidth
) {
jobTicks.push(currentJobTick);
return generateJobTickFromBucketWithStats([timestamp, stats]);
}

// Otherwise, continue merging data into the current job tick
currentJobTick = {
...currentJobTick,
endTs: timestamp,
stats: mergeStats(currentJobTick.stats, stats),
width: currentJobTick.width + 1,
};

// Ensure we render the last tick if it's the final bucket
if (i === data.length - 1) {
currentJobTick.roundedRight = true;
jobTicks.push(currentJobTick);
}

return currentJobTick;
},
null as JobTickDataWithStats | null
);

return jobTicks;
}

0 comments on commit 54be212

Please sign in to comment.