Skip to content

[FEATURE] add trace heatmap chart plugin - #724

Open
shahrokni wants to merge 1 commit into
mainfrom
feat/tracheheatmapchart2
Open

[FEATURE] add trace heatmap chart plugin#724
shahrokni wants to merge 1 commit into
mainfrom
feat/tracheheatmapchart2

Conversation

@shahrokni

@shahroknishahrokni commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Relates to perses/perses#3942

This is the bare minimumtrace heat map chart. All it needs is traces and the exponential base (default 2) to bucketize the traces according to their time and duration.

The x-axis and y-axis represent the time and duration buckets respectively. Both buckets are generated dynamically.
The following picture shows a base 2 bucketing.
It also supports base 10. Choosing a proper base may vary by different cases.

Demo

image

Same queries and traces, different bases

image

Next Steps?

Checklist

  • Pull request has a descriptive title and context useful to a reviewer.
  • Pull request title follows the [<catalog_entry>] <commit message> naming convention using one of the
    following catalog_entry values: FEATURE, ENHANCEMENT, BUGFIX, BREAKINGCHANGE, DOC,IGNORE.
  • All commits have DCO signoffs.

UI Changes

  • Changes that impact the UI include screenshots and/or screencasts of the relevant changes.
  • Code follows the UI guidelines.

@shahrokni
shahrokniforce-pushed the feat/tracheheatmapchart2 branch 3 times, most recently from c40e966 to 139f2eaCompareJuly 16, 2026 12:35
@shahrokni
shahrokniforce-pushed the feat/tracheheatmapchart2 branch 5 times, most recently from b33097f to cad2d04CompareJuly 21, 2026 12:18
Comment threadtraceheatmapchart/schemas/traceheatmapchart.json Outdated
Comment threadtraceheatmapchart/src/traceheatmap-chart-model.ts Outdated
Comment threadtraceheatmapchart/src/traceheatmap-time-bucket-util.ts
Comment threadtraceheatmapchart/src/traceheatmap-time-bucket-util.ts
@shahrokni
shahrokniforce-pushed the feat/tracheheatmapchart2 branch 3 times, most recently from e90da96 to 30c30f1CompareJuly 21, 2026 21:57
@shahrokni
shahrokniforce-pushed the feat/tracheheatmapchart2 branch from 30c30f1 to 69d4258CompareJuly 21, 2026 22:01
@shahrokni
shahrokni marked this pull request as ready for review July 21, 2026 22:07
@shahrokni
shahrokni requested review from a team, AntoineThebaud and Nexucis as code ownersJuly 21, 2026 22:07
@shahrokni
shahrokniforce-pushed the feat/tracheheatmapchart2 branch from 69d4258 to ec0a324CompareJuly 24, 2026 09:33
Comment threadscripts/npm/npm.go Outdated

func MustGetWorkspaces(dirPath string) []string {
excludedWorkspaces := []string{"e2e"}
excludedWorkspaces := []string{"e2e", "traceheatmapchart"}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

@shahroknishahrokniJul 27, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment this plugin does not include any Go files. Therefore, the pipeline throws an exception. For now, we can exclude the plugin from this part of the pipeline.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure why the plugin should not include go files. At least the sdk related files should exist.

Comment threadtraceheatmapchart/.cjs.swcrc Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need these files as they are included in the repo root.

Comment threadtraceheatmapchart/.swcrc Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need these files as they are included in the repo root.

Comment threadtraceheatmapchart/.gitignore Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need these files as they are included in the repo root.

Comment threadtraceheatmapchart/.eslintrc.js Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need these files as they are included in the repo root.

Comment threadtraceheatmapchart/LICENSE Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need these files as they are included in the repo root.

Comment threadtraceheatmapchart/go.mod Outdated
@@ -0,0 +1,30 @@
module github.com/perses/plugins/prometheus

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
module github.com/perses/plugins/prometheus
module github.com/perses/plugins/traceheatmapchart

Comment threadtraceheatmapchart/cue.mod/module.cue Outdated
@@ -0,0 +1,17 @@
module: "github.com/perses/traceheatmapchart@v0"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
module: "github.com/perses/traceheatmapchart@v0"
module: "github.com/perses/plugins/traceheatmapchart@v0"

Comment threadtraceheatmapchart/package.json Outdated
@@ -0,0 +1,63 @@
{
"name": "@perses/traceheatmap-chart-plugin",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"name": "@perses/traceheatmap-chart-plugin",
"name": "@perses-dev/traceheatmap-chart-plugin",

})(),
});

return `<div style="

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this HTML is invalid, was this ran to check?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Surprisingly the HTML works just fine although it has some syntax issue, funny! Let me check.
I also found out this can fall under the XSS attack! Let's see if I can improve it.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

scale: 'hourly',
getBuckets: (start, end, local) => {
const generateHourlyLabels = (ts: number) => {
const formatter = new Intl.DateTimeFormat('en-US', { hour: 'numeric' });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we avoid to hardcode the en-US locale?

elementId: generateId(column, row, 'min'),
value: ((): string | undefined => {
const indexes = bucketToFlatResultsMap[column]?.[row];
if (!indexes) return undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since this does not guard against empty arrays, what would be displayed?

@shahroknishahrokniAug 5, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understood your point right,
That should be fine to return undefined. When the Tooltip html is being built the undefined values are scaped by the filter

returnbucketDetails.filter((i)=>i.value)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I meant is that indexes can be [] which is thuthy

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the fix

image

// See the License for the specific language governing permissions and
// limitations under the License.

export { getPluginModule } from './getPluginModule';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are missing component exports here.

Comment threadtraceheatmapchart/package.json Outdated
"dev": "rsbuild dev",
"build": "npm run build-mf && concurrently \"npm:build:*\"",
"build-mf": "rsbuild build",
"build:cjs": "swc ./src -d dist/lib/cjs --strip-leading-paths --config-file .cjs.swcrc",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure why are we re defining the cjs.swcrc if there is one at the root that can be re used, like other plugins already do.

resultsStartTimes: Array<Pick<TraceSearchResult, 'startTimeUnixMs' | 'durationMs'>>,
local?: string
): TraceHeatMapTimeBucket => {
const startTimeStamp = min(...resultsStartTimes.map((i) => i.startTimeUnixMs));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could create a stack overflow with many traces, the min and max functions don't take arrays, they are expanded as variadic parameters, but the V8's has limits on how many parameters you can add. It will be better to use a plain loop and calculate the max and min at the same time.

@shahrokni
shahrokniforce-pushed the feat/tracheheatmapchart2 branch 5 times, most recently from 862d55b to 62c94d1CompareAugust 5, 2026 15:27
const indexes = bucketToFlatResultsMap[column]?.[row];
if (!indexes?.length) return undefined;
return (
max(...indexes.filter((idx) => flatResults[idx] !== undefined).map((idx) => flatResults[idx]!.durationMs)) + ''

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still doing a sort over a large amount of variadic params

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry missed!

@shahrokni
shahrokniforce-pushed the feat/tracheheatmapchart2 branch from 62c94d1 to 2a897cdCompareAugust 6, 2026 10:59
Signed-off-by: Mahmoud Shahrokni <seyedmahmoud.shahrokni@amadeus.com>
Signed-off-by: Mahmoud Shahrokni <seyedmahmoud.shahrokni@amadeus.com>
Signed-off-by: Mahmoud Shahrokni <seyedmahmoud.shahrokni@amadeus.com>
Signed-off-by: Mahmoud Shahrokni <seyedmahmoud.shahrokni@amadeus.com>
Signed-off-by: Mahmoud Shahrokni <seyedmahmoud.shahrokni@amadeus.com>
Signed-off-by: Mahmoud Shahrokni <seyedmahmoud.shahrokni@amadeus.com>
Signed-off-by: Mahmoud Shahrokni <seyedmahmoud.shahrokni@amadeus.com>
Signed-off-by: Mahmoud Shahrokni <seyedmahmoud.shahrokni@amadeus.com>
Signed-off-by: Mahmoud Shahrokni <seyedmahmoud.shahrokni@amadeus.com>
Signed-off-by: Mahmoud Shahrokni <seyedmahmoud.shahrokni@amadeus.com>
Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com>
Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com>
Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com>
Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com>
Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com>
Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com>
Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com>
Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com>
Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com>
Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com>
Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com>
Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com>
Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com>
Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com>
Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com>
Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com>
Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com>
Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com>
Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com>
@shahrokni
shahrokniforce-pushed the feat/tracheheatmapchart2 branch from 2a897cd to f114a88CompareAugust 6, 2026 11:11
export const escapeHtml = (value: string): string => {
return value
.replaceAll('&', '&amp;')
.replaceAll('<', '&alt;')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.replaceAll('<','&alt;')
.replaceAll('<','&lt;')

});

return `<div style="
id=${generateId(column, row, 'container')}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this id inside the css? This seems to be error prone, is there a more maintable way to build this HTML?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are also no tests around this function, so we are blind on the issues.


export type FlatQueryResults = TraceSearchResult[];

export const getHeatmapChartData = (flatQueryResults: FlatQueryResults, base: ExponentialBase): HeatmapChartData => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems the tests are too shallow, they are not actually testing how this function groups the data. For example, what happens with traces that have 0 duration?

minBucketCount = bucketCount;
}

bucketToFlatResultsMap[timeBucketIndex]![durationBucketIndex]!.push(i);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should avoid using type assertions with ! what if duration bucket is empty?

Comment on lines +90 to +92
if (i === buckets.length) {
break;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this if we are using a for loop with a condition?

if (i === buckets.length) {
break;
}
if (buckets[i]! > buckets[i + 1]!) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this compare the start field?

describe('escapeHtml', () => {
[
{ ch: '&', substitute: '&amp;' },
{ ch: '<', substitute: '&alt;' },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{ch: '<',substitute: '&alt;'},
{ch: '<',substitute: '&lt;'},

const {
queryResults,
spec: {
bucketSettings: { base },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we guarantee bucketSettings is always there?

);
}

const option: EChartsOption = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is built on every render and might cause unnecessary re configurations

getBuckets: (start: number, end: number, local?: string): TraceHeatMapTimeBucket => {
/* optimized using a js closure to avoid creating Intl.DateTimeFormat in every iteration */
const generateMonthlyLabel = (ts: number) => {
const formatter = new Intl.DateTimeFormat(local, { month: 'long' });

@jgbernalpjgbernalpAug 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is created on every iteration as the closure is inside the generateMonthlyLabel function. If reuse is needed or optimization it should be inside the getBuckets function.

constformatter=newIntl.DateTimeFormat(local,{month: 'long'});constgenerateMonthlyLabel=(ts: number): string=>{returnformatter.format(ts);};

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@shahrokni@jgbernalp