diff --git a/package-lock.json b/package-lock.json index e17a5f26b..c5d5d775c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "tracetable", "tracingganttchart", "victorialogs", + "traceheatmapchart", "e2e" ], "devDependencies": { @@ -4051,6 +4052,10 @@ "resolved": "tracetable", "link": true }, + "node_modules/@perses-dev/traceheatmap-chart-plugin": { + "resolved": "traceheatmapchart", + "link": true + }, "node_modules/@perses-dev/tracing-gantt-chart-plugin": { "resolved": "tracingganttchart", "link": true @@ -17662,6 +17667,28 @@ "use-resize-observer": "^9.0.0" } }, + "traceheatmapchart": { + "name": "@perses-dev/traceheatmap-chart-plugin", + "version": "0.1.0", + "license": "Apache-2.0", + "peerDependencies": { + "@emotion/react": "^11.7.1", + "@emotion/styled": "^11.6.0", + "@hookform/resolvers": "^3.2.0", + "@perses-dev/components": "^0.54.0-beta.11", + "@perses-dev/dashboards": "^0.54.0-beta.11", + "@perses-dev/plugin-system": "^0.54.0-beta.11", + "@perses-dev/spec": "^0.2.0-beta.6", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", + "echarts": "5.5.0", + "immer": "^10.1.1", + "lodash": "^4.17.21", + "react": "^17.0.2 || ^18.0.0", + "react-dom": "^17.0.2 || ^18.0.0", + "use-resize-observer": "^9.0.0" + } + }, "tracetable": { "name": "@perses-dev/trace-table-plugin", "version": "0.11.0", diff --git a/package.json b/package.json index 55393c45c..99c3e79be 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "tracetable", "tracingganttchart", "victorialogs", + "traceheatmapchart", "e2e" ], "peerDependencies": { diff --git a/traceheatmapchart/README.md b/traceheatmapchart/README.md new file mode 100644 index 000000000..d23afdec4 --- /dev/null +++ b/traceheatmapchart/README.md @@ -0,0 +1,43 @@ +# Plugin Module: traceheatmapchart + +### How to install + +This plugin requires react and react-dom 18 + +Install peer dependencies: + +```bash +npm install react@18 react-dom@18 +``` + +Install the plugin: + +```bash +npm install @my-org/traceheatmapchart +``` + +The Perses UI packages your plugin depends on are now maintained in the [`perses/shared`](https://github.com/perses/shared) repository. If you need to develop against local copies of those packages, follow the linking instructions in that repo. + +## Development + +### Setup + +Install dependencies: + +```bash +npm install +``` + +### Get Started + +Start the dev server: + +```bash +npm run dev +``` + +Build the plugin for distribution: + +```bash +npm run build +``` diff --git a/traceheatmapchart/cue.mod/module.cue b/traceheatmapchart/cue.mod/module.cue new file mode 100644 index 000000000..5f35b5e54 --- /dev/null +++ b/traceheatmapchart/cue.mod/module.cue @@ -0,0 +1,7 @@ +module: "github.com/perses/plugins/traceheatmapchart@v0" +language: { + version: "v0.15.1" +} +source: { + kind: "git" +} diff --git a/traceheatmapchart/go.mod b/traceheatmapchart/go.mod new file mode 100644 index 000000000..fd42e1e7c --- /dev/null +++ b/traceheatmapchart/go.mod @@ -0,0 +1,30 @@ +module github.com/perses/plugins/traceheatmapchart + +go 1.23.4 + +require github.com/perses/perses v0.50.1 + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-jose/go-jose/v4 v4.0.4 // indirect + github.com/jpillora/backoff v1.0.0 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/muhlemmer/gu v0.3.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect + github.com/prometheus/client_golang v1.20.5 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.61.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/zitadel/oidc/v3 v3.33.1 // indirect + github.com/zitadel/schema v1.3.0 // indirect + golang.org/x/crypto v0.37.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/oauth2 v0.24.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.24.0 // indirect + google.golang.org/protobuf v1.35.2 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/traceheatmapchart/go.sum b/traceheatmapchart/go.sum new file mode 100644 index 000000000..4e95fbdaf --- /dev/null +++ b/traceheatmapchart/go.sum @@ -0,0 +1,66 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= +github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= +github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nexucis/lamenv v0.5.2 h1:tK/u3XGhCq9qIoVNcXsK9LZb8fKopm0A5weqSRvHd7M= +github.com/nexucis/lamenv v0.5.2/go.mod h1:HusJm6ltmmT7FMG8A750mOLuME6SHCsr2iFYxp5fFi0= +github.com/perses/perses v0.50.1 h1:ySqFYu+/WXVWpDfSBZISN49m5docbrnNvjom7Ym9iNg= +github.com/perses/perses v0.50.1/go.mod h1:L6bykOUCMAI6CzGSMpK5EWT9ghBUHKxRP91LDCPpXW4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ= +github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/zitadel/oidc/v3 v3.33.1 h1:e3w9PDV0Mh50/ZiJWtzyT0E4uxJ6RXll+hqVDnqGbTU= +github.com/zitadel/oidc/v3 v3.33.1/go.mod h1:zkoZ1Oq6CweX3BaLrftLEGCs6YK6zDpjjVGZrP10AWU= +github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= +github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/traceheatmapchart/jest.config.ts b/traceheatmapchart/jest.config.ts new file mode 100644 index 000000000..5190fb4de --- /dev/null +++ b/traceheatmapchart/jest.config.ts @@ -0,0 +1,23 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { Config } from '@jest/types'; +import shared from '../jest.shared'; + +const jestConfig: Config.InitialOptions = { + ...shared, + + setupFilesAfterEnv: [...(shared.setupFilesAfterEnv ?? []), '/src/setup-tests.ts'], +}; + +export default jestConfig; diff --git a/traceheatmapchart/package.json b/traceheatmapchart/package.json new file mode 100644 index 000000000..0f687de5c --- /dev/null +++ b/traceheatmapchart/package.json @@ -0,0 +1,63 @@ +{ + "name": "@perses-dev/traceheatmap-chart-plugin", + "version": "0.1.0", + "license": "Apache-2.0", + "homepage": "https://github.com/perses/plugins/blob/main/README.md", + "repository": { + "type": "git", + "url": "git+https://github.com/perses/plugins.git" + }, + "bugs": { + "url": "https://github.com/perses/plugins/issues" + }, + "scripts": { + "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", + "build:esm": "swc ./src -d dist/lib --strip-leading-paths --config-file ../.swcrc", + "build:types": "tsc --project tsconfig.build.json", + "lint": "eslint src --ext .ts,.tsx", + "test": "cross-env LC_ALL=C TZ=UTC jest", + "type-check": "tsc --noEmit" + }, + "main": "lib/cjs/index.js", + "module": "lib/index.js", + "types": "lib/index.d.ts", + "peerDependencies": { + "@emotion/react": "^11.7.1", + "@emotion/styled": "^11.6.0", + "@hookform/resolvers": "^3.2.0", + "@perses-dev/components": "^0.54.0-beta.11", + "@perses-dev/dashboards": "^0.54.0-beta.11", + "@perses-dev/plugin-system": "^0.54.0-beta.11", + "@perses-dev/spec": "^0.2.0-beta.6", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", + "echarts": "5.5.0", + "lodash": "^4.17.21", + "react": "^17.0.2 || ^18.0.0", + "react-dom": "^17.0.2 || ^18.0.0", + "use-resize-observer": "^9.0.0", + "immer": "^10.1.1" + }, + "files": [ + "lib/**/*", + "__mf/**/*", + "mf-manifest.json", + "mf-stats.json" + ], + "perses": { + "plugins": [ + { + "kind": "Panel", + "spec": { + "display": { + "name": "Traceheatmap Chart" + }, + "name": "Traceheatmapchart" + } + } + ] + } +} diff --git a/traceheatmapchart/rsbuild.config.ts b/traceheatmapchart/rsbuild.config.ts new file mode 100644 index 000000000..7984dc2fa --- /dev/null +++ b/traceheatmapchart/rsbuild.config.ts @@ -0,0 +1,42 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { pluginReact } from '@rsbuild/plugin-react'; +import { createConfigForPlugin } from '../rsbuild.shared'; + +export default createConfigForPlugin({ + name: 'Traceheatmapchart', + rsbuildConfig: { + server: { port: 3023 }, + plugins: [pluginReact()], + }, + moduleFederation: { + exposes: { + './Traceheatmapchart': './src/TraceheatmapChart.ts', + }, + shared: { + react: { requiredVersion: '18.2.0', singleton: true }, + 'react-dom': { requiredVersion: '18.2.0', singleton: true }, + echarts: { singleton: true }, + 'date-fns': { singleton: true }, + 'date-fns-tz': { singleton: true }, + lodash: { singleton: true }, + '@perses-dev/components': { singleton: true }, + '@perses-dev/dashboards': { singleton: true }, + '@perses-dev/plugin-system': { singleton: true }, + '@emotion/react': { requiredVersion: '^11.11.3', singleton: true }, + '@emotion/styled': { singleton: true }, + '@hookform/resolvers': { singleton: true }, + }, + }, +}); diff --git a/traceheatmapchart/schemas/traceheatmapchart.cue b/traceheatmapchart/schemas/traceheatmapchart.cue new file mode 100644 index 000000000..3ff425fdc --- /dev/null +++ b/traceheatmapchart/schemas/traceheatmapchart.cue @@ -0,0 +1,24 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +kind: "Traceheatmapchart" + +spec: close({ + bucketSettings: #bucketSettings +}) + +#bucketSettings: { + base: *2 | 10 +} diff --git a/traceheatmapchart/sdk/go/options.go b/traceheatmapchart/sdk/go/options.go new file mode 100644 index 000000000..47bc7aa4d --- /dev/null +++ b/traceheatmapchart/sdk/go/options.go @@ -0,0 +1,26 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package traceheatmapchart + +import "fmt" + +func Base(bucketBase BucketBase) Option { + return func(builder *Builder) error { + if bucketBase != BucketBaseTwo && bucketBase != BucketBaseTen { + return fmt.Errorf("invalid bucket base: %d", bucketBase) + } + builder.BucketSettings.Base = bucketBase + return nil + } +} diff --git a/traceheatmapchart/sdk/go/traceheatmapchart.go b/traceheatmapchart/sdk/go/traceheatmapchart.go new file mode 100644 index 000000000..c9fd24bf3 --- /dev/null +++ b/traceheatmapchart/sdk/go/traceheatmapchart.go @@ -0,0 +1,70 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package traceheatmapchart + +import "github.com/perses/perses/go-sdk/panel" + +const PluginKind = "Traceheatmapchart" + +type BucketBase int + +const ( + BucketBaseTwo BucketBase = 2 + BucketBaseTen BucketBase = 10 +) + +type BucketSettings struct { + Base BucketBase `json:"base,omitempty" yaml:"base,omitempty"` +} + +type PluginSpec struct { + BucketSettings BucketSettings `json:"bucketSettings" yaml:"bucketSettings"` +} + +type Option func(plugin *Builder) error + +type Builder struct { + PluginSpec `json:",inline" yaml:",inline"` +} + +func create(options ...Option) (Builder, error) { + builder := &Builder{ + PluginSpec: PluginSpec{}, + } + + defaults := []Option{ + Base(BucketBaseTwo), + } + + for _, opt := range append(defaults, options...) { + if err := opt(builder); err != nil { + return *builder, err + } + } + + return *builder, nil +} + +func Chart(options ...Option) panel.Option { + return func(builder *panel.Builder) error { + plugin, err := create(options...) + if err != nil { + return err + } + + builder.Spec.Plugin.Kind = PluginKind + builder.Spec.Plugin.Spec = plugin.PluginSpec + return nil + } +} diff --git a/traceheatmapchart/sdk/go/traceheatmapchart_test.go b/traceheatmapchart/sdk/go/traceheatmapchart_test.go new file mode 100644 index 000000000..2be479e6b --- /dev/null +++ b/traceheatmapchart/sdk/go/traceheatmapchart_test.go @@ -0,0 +1,88 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package traceheatmapchart + +import ( + "encoding/json" + "testing" + + "github.com/perses/perses/go-sdk/panel" +) + +func TestChart(t *testing.T) { + tests := []struct { + title string + options []Option + expected string + wantError bool + }{ + {title: "return default panel builder", options: nil, expected: "{\"bucketSettings\":{\"base\":2}}", wantError: false}, + {title: "return panel builder with base 2", options: []Option{Base(BucketBaseTwo)}, expected: "{\"bucketSettings\":{\"base\":2}}", wantError: false}, + {title: "return panel builder with base 10", options: []Option{Base(BucketBaseTen)}, expected: "{\"bucketSettings\":{\"base\":10}}", wantError: false}, + {title: "return panel builder with base 10", options: []Option{Base(7)}, expected: "", wantError: true}, + } + + for _, tt := range tests { + t.Run(tt.title, func(t *testing.T) { + build := Chart(tt.options...) + panelBuilder := &panel.Builder{} + err := build(panelBuilder) + + if err != nil { + if !tt.wantError { + t.Fatalf("build returned unexpected error %v", err) + } + return + } + + actual, err := json.Marshal(panelBuilder.Spec.Plugin.Spec) + if err != nil { + t.Fatalf("marshal failed: %v", err) + } + if string(actual) != tt.expected { + t.Fatalf("received %s want %s", string(actual), tt.expected) + } + }) + } +} + +func TestCreateUsesDefaultBase(t *testing.T) { + builder, err := create() + if err != nil { + t.Fatalf("create returned an unexpected error: %v", err) + } + + if builder.BucketSettings.Base != BucketBaseTwo { + t.Errorf("default base = %d, want %d", builder.BucketSettings.Base, BucketBaseTwo) + } +} + +func TestCreateOverridesDefaultBase(t *testing.T) { + builder, err := create(Base(BucketBaseTen)) + if err != nil { + t.Fatalf("create returned unexpected error: %v", err) + } + + if builder.BucketSettings.Base != BucketBaseTen { + t.Errorf("base = %d, want %d", builder.BucketSettings.Base, BucketBaseTen) + } +} + +func TestCreateReturnOptionError(t *testing.T) { + _, err := create(Base(7)) + + if err == nil { + t.Fatalf("create expected an error, got nil") + } +} diff --git a/traceheatmapchart/src/TraceheatmapChart.ts b/traceheatmapchart/src/TraceheatmapChart.ts new file mode 100644 index 000000000..e77a6d074 --- /dev/null +++ b/traceheatmapchart/src/TraceheatmapChart.ts @@ -0,0 +1,25 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { PanelPlugin } from '@perses-dev/plugin-system'; +import { createInitialTraceheatmapChartOptions, TraceheatmapOptions } from './models/traceheatmap-chart-model'; +import { TraceheatmapSettingsEditor } from './TraceheatmapSettingsEditor'; + +import { TraceheatmapChartPanel, TraceheatmapChartProps } from './TraceheatmapChartPanel'; + +export const Traceheatmapchart: PanelPlugin = { + PanelComponent: TraceheatmapChartPanel, + supportedQueryTypes: ['TraceQuery'], + panelOptionsEditorComponents: [{ label: 'General Settings', content: TraceheatmapSettingsEditor }], + createInitialOptions: createInitialTraceheatmapChartOptions, +}; diff --git a/traceheatmapchart/src/TraceheatmapChartPanel.tsx b/traceheatmapchart/src/TraceheatmapChartPanel.tsx new file mode 100644 index 000000000..f17328464 --- /dev/null +++ b/traceheatmapchart/src/TraceheatmapChartPanel.tsx @@ -0,0 +1,146 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { PanelProps } from '@perses-dev/plugin-system'; +import { ReactElement, useMemo, useRef } from 'react'; +import { TraceData } from '@perses-dev/spec'; +import { EChart, useChartsContext } from '@perses-dev/components'; +import { ECharts as EChartsInstance } from 'echarts/core'; +import { EChartsOption } from 'echarts'; +import { Box, Typography } from '@mui/material'; +import { TraceheatmapOptions } from './models/traceheatmap-chart-model'; +import { FlatQueryResults, getHeatmapChartData, getPlotterData } from './traceheatmap-chart-data-util'; +import { createBucketToolTip } from './traceheatmap-tooltip-util'; + +export type TraceheatmapChartProps = PanelProps; + +export const TraceheatmapChartPanel = (props: TraceheatmapChartProps): ReactElement => { + const { + queryResults, + spec: { + bucketSettings: { base }, + }, + } = props; + + const chartRef = useRef(); + const { chartsTheme } = useChartsContext(); + + const allFlatResults = useMemo(() => { + const allFlatResults: FlatQueryResults = []; + queryResults.forEach((qr) => { + (qr.data.searchResult || []).forEach((sr) => { + allFlatResults.push({ ...sr }); + }); + }); + return allFlatResults; + }, [queryResults]); + + const { bucketCountMap, bucketToFlatResultsMap, durationBuckets, maxBucketCount, minBucketCount, timeBuckets } = + useMemo(() => { + return getHeatmapChartData(allFlatResults, base); + }, [allFlatResults, base]); + + const data = useMemo(() => { + return getPlotterData(bucketCountMap, { showZero: false, zeroSubstitute: ' ' }); + }, [bucketCountMap]); + + if ( + [bucketCountMap, bucketToFlatResultsMap, timeBuckets, durationBuckets].some((i) => i === undefined || !i.length) + ) { + return ( + + No data + + ); + } + + const option: EChartsOption = { + tooltip: { + appendTo: 'body', + trigger: 'item', + confine: false, + formatter: (params) => { + return createBucketToolTip(params, { + bucketCountMap, + bucketToFlatResultsMap, + durationBuckets, + timeBuckets, + flatResults: allFlatResults, + }); + }, + }, + grid: { + top: 10, + right: 10, + bottom: 20, + left: 20, + }, + xAxis: { + type: 'category', + data: timeBuckets.map((tb) => tb.label), + splitArea: { + show: true, + }, + }, + yAxis: { + type: 'category', + data: durationBuckets.map((db) => db.label), + splitArea: { + show: true, + }, + }, + visualMap: { + min: minBucketCount, + max: maxBucketCount, + calculable: true, + show: false, + }, + series: [ + { + name: 'Traces', + type: 'heatmap', + data: data, + label: { + show: true, + }, + emphasis: { + itemStyle: { + shadowBlur: 10, + shadowColor: 'rgba(0, 0, 0, 0.5)', + }, + }, + }, + ], + }; + + return ( + + + + ); +}; diff --git a/traceheatmapchart/src/TraceheatmapSettingsEditor.tsx b/traceheatmapchart/src/TraceheatmapSettingsEditor.tsx new file mode 100644 index 000000000..bdad29e22 --- /dev/null +++ b/traceheatmapchart/src/TraceheatmapSettingsEditor.tsx @@ -0,0 +1,63 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { OptionsEditorProps } from '@perses-dev/plugin-system'; +import { ReactElement, useCallback } from 'react'; +import { + OptionsEditorColumn, + OptionsEditorControl, + OptionsEditorGrid, + OptionsEditorGroup, +} from '@perses-dev/components'; +import { FormControl, InputLabel, MenuItem, Select, SelectChangeEvent } from '@mui/material'; +import { TraceheatmapOptions } from './models/traceheatmap-chart-model'; +import { ExponentialBase } from './traceheatmap-duration-bucket-util'; + +export type TraceheatmapSettingsEditorProps = OptionsEditorProps; + +export const TraceheatmapSettingsEditor = (props: TraceheatmapSettingsEditorProps): ReactElement => { + const { onChange, value } = props; + const bases: ExponentialBase[] = [2, 10]; + + const handleChange = useCallback( + (event: SelectChangeEvent) => { + const base = Number(event.target.value) as ExponentialBase; + onChange({ ...value, bucketSettings: { ...value.bucketSettings, base } }); + }, + [onChange, value] + ); + + return ( + + + + + Base + + + } + /> + + + + ); +}; diff --git a/traceheatmapchart/src/bootstrap.tsx b/traceheatmapchart/src/bootstrap.tsx new file mode 100644 index 000000000..06f1ffc5a --- /dev/null +++ b/traceheatmapchart/src/bootstrap.tsx @@ -0,0 +1,18 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react'; +import ReactDOM from 'react-dom/client'; + +const root = ReactDOM.createRoot(document.getElementById('root')!); +root.render(); diff --git a/traceheatmapchart/src/env.d.ts b/traceheatmapchart/src/env.d.ts new file mode 100644 index 000000000..01f4e7302 --- /dev/null +++ b/traceheatmapchart/src/env.d.ts @@ -0,0 +1,14 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// diff --git a/traceheatmapchart/src/getPluginModule.ts b/traceheatmapchart/src/getPluginModule.ts new file mode 100644 index 000000000..cab8b384b --- /dev/null +++ b/traceheatmapchart/src/getPluginModule.ts @@ -0,0 +1,30 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { PluginModuleResource, PluginModuleSpec } from '@perses-dev/plugin-system'; +import packageJson from '../package.json'; + +/** + * Returns the plugin module information from package.json + */ +export function getPluginModule(): PluginModuleResource { + const { name, version, perses } = packageJson; + return { + kind: 'PluginModule', + metadata: { + name, + version, + }, + spec: perses as PluginModuleSpec, + }; +} diff --git a/traceheatmapchart/src/index-federation.ts b/traceheatmapchart/src/index-federation.ts new file mode 100644 index 000000000..df7f0bd82 --- /dev/null +++ b/traceheatmapchart/src/index-federation.ts @@ -0,0 +1,14 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import('./bootstrap'); diff --git a/traceheatmapchart/src/index.ts b/traceheatmapchart/src/index.ts new file mode 100644 index 000000000..d2273c45d --- /dev/null +++ b/traceheatmapchart/src/index.ts @@ -0,0 +1,16 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export { getPluginModule } from './getPluginModule'; +export * from './models'; +export * from './TraceheatmapChart'; diff --git a/traceheatmapchart/src/models/index.ts b/traceheatmapchart/src/models/index.ts new file mode 100644 index 000000000..ff3b06baa --- /dev/null +++ b/traceheatmapchart/src/models/index.ts @@ -0,0 +1,14 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export * from './traceheatmap-chart-model'; diff --git a/traceheatmapchart/src/models/traceheatmap-chart-model.ts b/traceheatmapchart/src/models/traceheatmap-chart-model.ts new file mode 100644 index 000000000..b4342627e --- /dev/null +++ b/traceheatmapchart/src/models/traceheatmap-chart-model.ts @@ -0,0 +1,26 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { ExponentialBase } from '../traceheatmap-duration-bucket-util'; + +export interface BucketSettings { + base: ExponentialBase; +} + +export interface TraceheatmapOptions { + bucketSettings: BucketSettings; +} + +export const createInitialTraceheatmapChartOptions = (): TraceheatmapOptions => ({ + bucketSettings: { base: 2 }, +}); diff --git a/traceheatmapchart/src/setup-tests.ts b/traceheatmapchart/src/setup-tests.ts new file mode 100644 index 000000000..d43de32e0 --- /dev/null +++ b/traceheatmapchart/src/setup-tests.ts @@ -0,0 +1,17 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import '@testing-library/jest-dom'; + +// Always mock e-charts during tests since we don't have a proper canvas in jsdom +jest.mock('echarts/core'); diff --git a/traceheatmapchart/src/trace-search-result-mock.ts b/traceheatmapchart/src/trace-search-result-mock.ts new file mode 100644 index 000000000..9d60dcc8a --- /dev/null +++ b/traceheatmapchart/src/trace-search-result-mock.ts @@ -0,0 +1,815 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export const traceMock = [ + { + startTimeUnixMs: 1784192174685.3193, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192174654.7686, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192172653.8154, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192172642.9243, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192171742.1064, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192170841.6458, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192170690.9993, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192170660.736, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192169760.4045, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192169679.942, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192169649.6548, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192167648.3906, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192167497.7976, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192167486.031, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192166585.4404, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192166434.9548, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192166284.4407, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192166203.7334, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192166172.8992, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192165772.3674, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192165762.1357, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192164861.279, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192164460.4258, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192163559.9954, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192163409.605, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192162508.731, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192162107.858, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192161207.4866, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192161193.9077, + durationMs: 13, + }, + { + startTimeUnixMs: 1784192161163.3796, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192160262.685, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192159862.2695, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192159851.717, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192159451.1855, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192159050.4116, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192158649.8252, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192157749.162, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192152748.7302, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192151848.0571, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192151766.5947, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192151686.169, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192151675.7922, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192151665.0178, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192151654.413, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192151573.7412, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192149572.9575, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192148672.8562, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192148592.5503, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192148561.9368, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192147661.371, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192145661.0486, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192145630.3525, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192145479.727, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192145469.01, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192143468.5906, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192143458.0422, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192143428.0112, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192143417.49, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192143016.8562, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192142616.481, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192142534.5754, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192142504.277, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192142473.7092, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192142073.0256, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192140072.4106, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192140041.936, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192138040.5986, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192138009.2964, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192137858.8264, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192137848.1501, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192137447.6226, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192135446.7249, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192130446.2883, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192130045.8477, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192130015.036, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192128014.522, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192127934.071, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192122932.8218, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192122922.2854, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192122841.2324, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192120840.8254, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192120439.5728, + durationMs: 401, + }, + { + startTimeUnixMs: 1784192120355.559, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192118354.576, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192113354.0989, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192113343.55, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192111343.1006, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192111312.1196, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192111281.0774, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192111270.5344, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192111190.0708, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192111039.9849, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192109039.289, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192104038.8708, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192104028.1548, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192099027.4658, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192097026.6997, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192097016.131, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192096984.6975, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192096584.1492, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192096503.571, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192096352.4004, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192095451.448, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192090450.7217, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192088449.989, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192088439.3752, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192088407.0408, + durationMs: 32, + }, + { + startTimeUnixMs: 1784192088256.3264, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192086255.661, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192085855.1626, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192083854.3394, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192082953.9902, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192077953.517, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192077052.9263, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192076151.8887, + durationMs: 901, + }, + { + startTimeUnixMs: 1784192075250.7153, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192073249.3381, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192073098.1663, + durationMs: 151, + }, + { + startTimeUnixMs: 1784192072696.7864, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192072296.4514, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192072260.7258, + durationMs: 35, + }, + { + startTimeUnixMs: 1784192070259.9565, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192069359.8064, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192069329.0261, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192069318.2317, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192069287.2703, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192064286.857, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192062283.6328, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192062132.1172, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192062051.627, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192061647.1904, + durationMs: 403, + }, + { + startTimeUnixMs: 1784192059646.4438, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192059496.0369, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192059095.6033, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192059065.0618, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192058984.4597, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192056983.641, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192051983.162, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192051952.4688, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192051921.7188, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192049921.1357, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192049905.3608, + durationMs: 15, + }, + { + startTimeUnixMs: 1784192048998.6458, + durationMs: 906, + }, + { + startTimeUnixMs: 1784192048988.0898, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192046987.4468, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192046086.5837, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192045936.229, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192045925.6958, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192045915.179, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192045884.641, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192045853.962, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192044953.309, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192042952.8193, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192037952.4658, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192037941.9148, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192032941.4517, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192032040.8254, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192031960.1401, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192031949.7449, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192026948.759, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192026868.498, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192026838.063, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192024837.3977, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192024437.0999, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192024405.129, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192024394.2751, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192024313.8738, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192019313.7886, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192018413.3171, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192018012.782, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192017111.7844, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192017081.5464, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192017071.0217, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192016169.9194, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192015267.4995, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192010266.0486, + durationMs: 5001, + }, + { + startTimeUnixMs: 1784192010115.4226, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192008115.1465, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192008104.5806, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192007954.1602, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192007923.279, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192007022.5178, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192006122.1655, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192005970.3157, + durationMs: 151, + }, + { + startTimeUnixMs: 1784192005889.768, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192005489.311, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192005408.8762, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192003408.443, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192001408.0996, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784191996404.2163, + durationMs: 5001, + }, + { + startTimeUnixMs: 1784191996323.9153, + durationMs: 80, + }, + { + startTimeUnixMs: 1784191991323.4905, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784191991293.1313, + durationMs: 30, + }, + { + startTimeUnixMs: 1784191991262.6406, + durationMs: 30, + }, + { + startTimeUnixMs: 1784191986261.826, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784191981261.2673, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784191981230.6716, + durationMs: 30, + }, + { + startTimeUnixMs: 1784191981150.0059, + durationMs: 80, + }, + { + startTimeUnixMs: 1784191981139.4368, + durationMs: 10, + }, + { + startTimeUnixMs: 1784191979138.5269, + durationMs: 2000, + }, +]; diff --git a/traceheatmapchart/src/traceheatmap-chart-data-util.test.ts b/traceheatmapchart/src/traceheatmap-chart-data-util.test.ts new file mode 100644 index 000000000..2da02f847 --- /dev/null +++ b/traceheatmapchart/src/traceheatmap-chart-data-util.test.ts @@ -0,0 +1,27 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { traceMock } from './trace-search-result-mock'; +import { FlatQueryResults, getHeatmapChartData } from './traceheatmap-chart-data-util'; + +describe('traceheatmap-chart-data-util', () => { + const { bucketCountMap, bucketToFlatResultsMap, durationBuckets, timeBuckets } = getHeatmapChartData( + traceMock as FlatQueryResults, + 2 + ); + test('cellWeightMap, metaDataArray, timeBuckets, durationBuckets should not be undefined', () => { + [bucketCountMap, bucketToFlatResultsMap, timeBuckets, durationBuckets].forEach((i) => { + expect(i).not.toBeUndefined(); + }); + }); +}); diff --git a/traceheatmapchart/src/traceheatmap-chart-data-util.ts b/traceheatmapchart/src/traceheatmap-chart-data-util.ts new file mode 100644 index 000000000..0a56fd1d0 --- /dev/null +++ b/traceheatmapchart/src/traceheatmap-chart-data-util.ts @@ -0,0 +1,140 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { TraceSearchResult } from '@perses-dev/spec'; +import { ExponentialBase, getDurationBuckets, TraceHeatMapDurationBuckets } from './traceheatmap-duration-bucket-util'; +import { getTimeBuckets, TraceHeatMapTimeBucket } from './traceheatmap-time-bucket-util'; + +export type HeatmapChartData = { + bucketCountMap: number[][]; + bucketToFlatResultsMap: number[][][]; + timeBuckets: TraceHeatMapTimeBucket; + durationBuckets: TraceHeatMapDurationBuckets; + minBucketCount: number; + maxBucketCount: number; +}; + +export type PlotterOptions = { + showZero: boolean; + zeroSubstitute: string; +}; + +export type FlatQueryResults = TraceSearchResult[]; + +export const getHeatmapChartData = (flatQueryResults: FlatQueryResults, base: ExponentialBase): HeatmapChartData => { + /* e-chart heatmap uses the min and max for setting the color range from the most pale to darkest*/ + let minBucketCount = Infinity; + let maxBucketCount = -Infinity; + + /** + * Both buckets are generated according to the query results (traces) + * to avoid fixed length and sparse buckets + */ + const timeBuckets = getTimeBuckets( + flatQueryResults.map((r) => ({ durationMs: r.durationMs, startTimeUnixMs: r.startTimeUnixMs })) + ); + const durationBuckets = getDurationBuckets( + flatQueryResults.map((qst) => qst.durationMs), + base + ); + + /** + * initiate the bucket-count and flat-data-index maps + * the latter one is used to retrieve the details of query results from the received results + */ + const bucketCountMap: number[][] = []; + const bucketToFlatResultsMap: number[][][] = []; + + timeBuckets.forEach(() => { + const valueRow: number[] = []; + const metaDataMap: number[][] = []; + durationBuckets.forEach(() => { + valueRow.push(0); + metaDataMap.push([]); + }); + bucketCountMap.push(valueRow); + bucketToFlatResultsMap.push(metaDataMap); + }); + + /* find the row and column indexes to find the fit bucket for all records */ + for (let i = 0; i < flatQueryResults.length; i += 1) { + const { startTimeUnixMs, durationMs } = flatQueryResults[i]!; + let timeBucketIndex = 0; + let durationBucketIndex = 0; + + for (let j = 0; j < timeBuckets.length; j += 1) { + if (j === timeBuckets.length - 1) { + timeBucketIndex = j; + break; + } + if (startTimeUnixMs >= timeBuckets[j]!.startTimestamp && startTimeUnixMs < timeBuckets[j + 1]!.startTimestamp) { + timeBucketIndex = j; + break; + } + } + + for (let j = 0; j < durationBuckets.length; j += 1) { + if (j === durationBuckets.length - 1) { + durationBucketIndex = j; + break; + } + if (durationMs >= durationBuckets[j]!.start && durationMs < durationBuckets[j + 1]!.start) { + durationBucketIndex = j; + break; + } + } + + const bucketCount = bucketCountMap[timeBucketIndex]![durationBucketIndex]! + 1; + bucketCountMap[timeBucketIndex]![durationBucketIndex]! = bucketCount; + + if (bucketCount > maxBucketCount) { + maxBucketCount = bucketCount; + } + + if (bucketCount < minBucketCount) { + minBucketCount = bucketCount; + } + + bucketToFlatResultsMap[timeBucketIndex]![durationBucketIndex]!.push(i); + } + + return { + bucketCountMap, + bucketToFlatResultsMap, + timeBuckets, + durationBuckets, + minBucketCount, + maxBucketCount, + }; +}; + +/** + * prepares the appropriate e-chart heatmap data format + */ +export const getPlotterData = ( + cellWeightMap: number[][], + plotterOptions?: PlotterOptions +): Array> => { + const { showZero, zeroSubstitute } = plotterOptions || { showZero: false, zeroSubstitute: ' ' }; + const eChartData: Array> = []; + cellWeightMap.forEach((r, rIdx) => { + r.forEach((c, cIdx) => { + let value: string | number = c; + if (!showZero && !value) { + value = zeroSubstitute; + } + eChartData.push([rIdx, cIdx, value]); + }); + }); + return eChartData; +}; diff --git a/traceheatmapchart/src/traceheatmap-duration-bucket-util.test.ts b/traceheatmapchart/src/traceheatmap-duration-bucket-util.test.ts new file mode 100644 index 000000000..001e2a887 --- /dev/null +++ b/traceheatmapchart/src/traceheatmap-duration-bucket-util.test.ts @@ -0,0 +1,102 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { ExponentialBase, getDurationBuckets } from './traceheatmap-duration-bucket-util'; + +type TestBody = { durations: number[]; base: ExponentialBase; desc: string; exp: string; testPurpose: string }; + +describe('traceheatmap-duration-bucket-util', () => { + describe('generate buckets', () => { + const tests: TestBody[] = [ + { + durations: [1, 2, 3, 4, 5, 6, 7, 8], + base: 2, + desc: 'a simple test for exponential buckets demonstration', + exp: '1,2,4,8', + testPurpose: 'demonstration base 2 - helps to understand bucketing', + }, + { + durations: [1, 2, 3, 4, 5, 6, 7, 8], + base: 10, + desc: 'a simple test for exponential buckets demonstration', + exp: '1,10', + testPurpose: 'demonstration base 10 - helps to understand bucketing', + }, + { + durations: [65, 51, 50, 46, 29, 37, 31], + base: 2, + desc: 'should range from 16 to +128', + exp: '16,32,64,128', + testPurpose: 'realistic network latency', + }, + { + durations: [65, 51, 1500, 46, 29, 37, 31], + base: 2, + desc: 'should range from 16 to +2048', + exp: '16,32,64,128,256,512,1024,2048', + testPurpose: 'realistic network latency with a considerable lag', + }, + { + durations: [65, 51, 1500, 46, 29, 37, 31], + base: 10, + desc: 'should range from 10 to +10000', + exp: '10,100,1000,10000', + testPurpose: 'realistic network latency with a considerable lag', + }, + { + durations: [0.5, 0.4, 1, 10, 2], + base: 2, + desc: 'should range from 0.25 to +16', + exp: '0.25,0.5,1,2,4,8,16', + testPurpose: 'minimum is microseconds 0 { + const { durations, base, exp, desc, testPurpose } = tst; + const buckets = getDurationBuckets(durations, base); + test(`base:${base}-${desc}-${testPurpose}`, () => { + const starts = buckets.map((b) => b.start).join(','); + expect(starts).toBe(exp); + }); + + test('buckets should be sorted', () => { + let sorted = true; + for (let i = 0; i < buckets.length; i += 1) { + if (i === buckets.length) { + break; + } + if (buckets[i]! > buckets[i + 1]!) { + sorted = false; + break; + } + } + expect(sorted).toBe(true); + }); + }); + }); +}); diff --git a/traceheatmapchart/src/traceheatmap-duration-bucket-util.ts b/traceheatmapchart/src/traceheatmap-duration-bucket-util.ts new file mode 100644 index 000000000..bc147e6a9 --- /dev/null +++ b/traceheatmapchart/src/traceheatmap-duration-bucket-util.ts @@ -0,0 +1,63 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* shortened */ +const max = Math.max; +const min = Math.min; +const floor = Math.floor; +const ceil = Math.ceil; +const pow = Math.pow; + +/** + * From the mathematical perspective these are the two most common bases which could be used for exponential functions, though any number would be technically valid + * Both come with advantages and disadvantages. The base should be picked by the users accordingly. + * For instance, base 2 may be appropriate for the network latency, whereas 10 is much better for db operations + */ +export type ExponentialBase = 2 | 10; + +export type TraceHeatMapDurationBuckets = Array<{ start: number; label: string }>; + +const baseLog = (base: number, x: number): number => Math.log(x) / Math.log(base); + +export const getDurationBuckets = (durations: number[], base: ExponentialBase = 2): TraceHeatMapDurationBuckets => { + const cleanDuration = durations.filter((d) => d > 0); + const minDuration = ((): number => { + let minimum = Infinity; + for (const cd of cleanDuration) { + minimum = min(cd, minimum); + } + + return minimum; + })(); + + const maxDuration = ((): number => { + let maximum = -Infinity; + for (const cd of cleanDuration) { + maximum = max(cd, maximum); + } + + return maximum; + })(); + + const buckets: TraceHeatMapDurationBuckets = []; + + const startExponent = floor(baseLog(base, minDuration)); + const endExponent = ceil(baseLog(base, maxDuration)); + + for (let exp = startExponent; exp <= endExponent; exp++) { + const start = pow(base, exp); + const label = start < 1 ? `${start.toFixed(2)} ms` : `${start} ms`; + buckets.push({ start, label }); + } + return buckets; +}; diff --git a/traceheatmapchart/src/traceheatmap-time-bucket-util.test.ts b/traceheatmapchart/src/traceheatmap-time-bucket-util.test.ts new file mode 100644 index 000000000..e94ee3892 --- /dev/null +++ b/traceheatmapchart/src/traceheatmap-time-bucket-util.test.ts @@ -0,0 +1,186 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { findScale, MILLISECONDS_TIME_SCALE } from './traceheatmap-time-bucket-util'; + +const formatDate = (date: number): string | undefined => new Date(date).toISOString().split('T')[0]; +type TestBody = { start: number; end: number; desc: string; testPurpose: string; exp: string }; + +describe('traceheatmap-time-bucket-util', () => { + describe('scaling', () => { + describe('buckets', () => { + const tests: TestBody[] = [ + { + start: 1725896153082, + end: 1783956953082, + desc: 'should include 2024, 2025', + testPurpose: 'yearly scaled', + exp: '2024-2025,2025-2026', + }, + { + start: 1814400000000, + end: 1817078400000, + desc: 'should include July 2027', + testPurpose: 'monthly scaled', + exp: 'July', + }, + { + start: 1782864000000, + end: 1786492800000, + desc: 'should include July and August 2026', + testPurpose: 'stretched monthly scaled', + exp: 'July,August', + }, + { + start: 1782864000000, + end: 1789171200000, + desc: 'should include July, August, and September', + testPurpose: 'stretched monthly scaled', + exp: 'July,August,September', + }, + { + start: 1783987200000, + end: 1784592000000, + desc: 'should include 14th to 21st of July 2026', + testPurpose: 'weekly scaled', + exp: 'July 14 - July 21', + }, + { + start: 1783987200000, + end: 1784851200000, + desc: 'should include 14th to 28th of July 2026', + testPurpose: 'weekly scaled', + exp: 'July 14 - July 21,July 21 - July 28', + }, + { + start: 1784937600000, + end: 1785715200000, + desc: 'should include July 25th to 8th Aug 2026', + testPurpose: 'stretched weekly scaled', + exp: 'July 25 - August 1,August 1 - August 8', + }, + { + start: 1782864000000, + end: 1785456000000, + desc: 'should include the entire July 2026 and the beginning of August', + testPurpose: 'entire month - weekly scaled', + exp: 'July 1 - July 8,July 8 - July 15,July 15 - July 22,July 22 - July 29,July 29 - August 5', + }, + { + start: 1769904000000, + end: 1772236800000, + desc: 'should include the entire Feb (28 days) 2026', + testPurpose: 'entire Feb (28 days) - weekly scaled', + exp: 'February 1 - February 8,February 8 - February 15,February 15 - February 22,February 22 - March 1', + }, + { + start: 1706745600000, + end: 1709164800000, + desc: 'should include the entire Feb (29 days - leap year) 2024', + testPurpose: 'entire Feb (29 days) - weekly scaled', + exp: 'February 1 - February 8,February 8 - February 15,February 15 - February 22,February 22 - February 29', + }, + { + start: 1783987200000, + end: 1784091600000, + desc: 'should include week days', + testPurpose: 'daily scaled', + exp: 'Tuesday,Wednesday', + }, + { + start: 1783990800000, + end: 1784037600000, + desc: 'should include hours', + testPurpose: 'hourly scaled', + exp: '1 AM - 2 AM,2 AM - 3 AM,3 AM - 4 AM,4 AM - 5 AM,5 AM - 6 AM,6 AM - 7 AM,7 AM - 8 AM,8 AM - 9 AM,9 AM - 10 AM,10 AM - 11 AM,11 AM - 12 PM,12 PM - 1 PM,1 PM - 2 PM', + }, + { + start: 1784640900000, + end: 1784648220000, + desc: 'should include hours', + testPurpose: 'hourly scaled', + exp: '1 PM - 2 PM,2 PM - 3 PM,3 PM - 4 PM', + }, + { + start: 1784640900000, + end: 1784648700000, + desc: 'should include hours', + testPurpose: 'hourly scaled', + exp: '1 PM - 2 PM,2 PM - 3 PM,3 PM - 4 PM', + }, + { + start: 1783990800000, + end: 1784073599000, + desc: 'should include the entire hours of the day', + testPurpose: 'hourly scaled', + exp: '1 AM - 2 AM,2 AM - 3 AM,3 AM - 4 AM,4 AM - 5 AM,5 AM - 6 AM,6 AM - 7 AM,7 AM - 8 AM,8 AM - 9 AM,9 AM - 10 AM,10 AM - 11 AM,11 AM - 12 PM,12 PM - 1 PM,1 PM - 2 PM,2 PM - 3 PM,3 PM - 4 PM,4 PM - 5 PM,5 PM - 6 PM,6 PM - 7 PM,7 PM - 8 PM,8 PM - 9 PM,9 PM - 10 PM,10 PM - 11 PM,11 PM - 12 AM', + }, + { + start: 1784066400000, + end: 1784088000000, + desc: 'should include hours of 14th and 15th of July 2026', + testPurpose: 'stretched hourly scaled', + exp: '10 PM - 11 PM,11 PM - 12 AM,12 AM - 1 AM,1 AM - 2 AM,2 AM - 3 AM,3 AM - 4 AM', + }, + { + start: 1784088060000, + end: 1784088300000, + desc: 'should include minutes', + testPurpose: 'minutes scaled', + exp: '04:01-04:02,04:02-04:03,04:03-04:04,04:04-04:05', + }, + { + start: 1784159880000, + end: 1784160300000, + desc: 'should include minutes', + testPurpose: 'stretched minutes scaled', + exp: '23:58-23:59,23:59-00:00,00:00-00:01,00:01-00:02,00:02-00:03,00:03-00:04,00:04-00:05', + }, + { + start: 1784649599000, + end: 1784649605000, + desc: 'should include seconds', + testPurpose: 'seconds scaled', + exp: '59-0,0-1,1-2,2-3,3-4,4-5', + }, + ]; + + tests.forEach((tst) => { + const { start, end, exp, testPurpose } = tst; + + const { getBuckets } = MILLISECONDS_TIME_SCALE.find((i) => i.scale === findScale(start, end))!; + const buckets = getBuckets(start, end, 'en-US'); + + test(`from ${formatDate(start)} to ${formatDate(end)}: ${tst.desc} demonstrates: ${testPurpose}`, () => { + const labels = buckets.map((i) => i.label).join(','); + expect(labels).toBe(exp); + }); + + test(`generated buckets ranging from ${buckets[0]?.startTimestamp} to ${buckets[buckets.length - 1]?.startTimestamp} should be sorted ascendingly`, () => { + const startTimestamps = buckets.map((i) => i.startTimestamp); + let sortedBuckets = true; + for (let i = 0; i < startTimestamps.length; i += 1) { + if (i === startTimestamps.length - 1) { + break; + } + if (startTimestamps[i]! > startTimestamps[i + 1]!) { + sortedBuckets = false; + break; + } + } + expect(sortedBuckets).toBe(true); + }); + }); + }); + }); +}); diff --git a/traceheatmapchart/src/traceheatmap-time-bucket-util.ts b/traceheatmapchart/src/traceheatmap-time-bucket-util.ts new file mode 100644 index 000000000..1db26a68c --- /dev/null +++ b/traceheatmapchart/src/traceheatmap-time-bucket-util.ts @@ -0,0 +1,230 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { TraceSearchResult } from '@perses-dev/spec'; + +/* shortened */ +const trunc = Math.trunc; +const ciel = Math.ceil; +const max = Math.max; +const min = Math.min; + +export type TraceHeatMapTimeBucket = Array<{ label: string; startTimestamp: number }>; + +export type Scale = 'yearly' | 'monthly' | 'weekly' | 'daily' | 'hourly' | 'minutes' | 'seconds'; + +export type TraceHeatMapTimeScale = { + scale: Scale; + getBuckets: (start: number, end: number, local?: string) => TraceHeatMapTimeBucket; +}; + +export const TIME_IN_MS = { + get second(): number { + return 1000; + }, + get minute(): number { + return 60 * this.second; + }, + get hour(): number { + return 60 * this.minute; + }, + get day(): number { + return 24 * this.hour; + }, + get week(): number { + return 7 * this.day; + }, + get month(): number { + /** + * A month may include 28 to 31 days. + * To ease the scaling process and generate buckets, we keep the maximum which is 31 days. + * Therefore, months with less than 31 days simply fall under the weekly scaling and produce weekly buckets + */ + return 31 * this.day; + }, + get year(): number { + /** + * Any value greater than equal the year definition is scaled yearly. + * This includes the leap years which have 366 days. + */ + return 365 * this.day; + }, +}; + +export const findScale = (start: number, end: number): Scale | undefined => { + let scale: Scale | undefined = undefined; + const diff = end - start; + + if (diff <= 0) return undefined; + + if (diff >= TIME_IN_MS.year) { + scale = 'yearly'; + } else if (diff >= TIME_IN_MS.month) { + scale = 'monthly'; + } else if (diff >= TIME_IN_MS.week) { + scale = 'weekly'; + } else if (diff >= TIME_IN_MS.day) { + scale = 'daily'; + } else if (diff >= TIME_IN_MS.hour) { + scale = 'hourly'; + } else if (diff >= TIME_IN_MS.minute) { + scale = 'minutes'; + } else if (diff > 0) { + scale = 'seconds'; + } + return scale; +}; + +type LabelGenerator = ((ts: number, local?: string) => string) | ((ts: number, local?: string) => () => string); + +const produceBuckets = ( + scale: keyof typeof TIME_IN_MS, + labelGenerator: LabelGenerator, + start: number, + end: number, + local?: string +): TraceHeatMapTimeBucket => { + const diff = trunc(end) - trunc(start); + const bucketCounts = ciel(diff / TIME_IN_MS[scale]); + const firstLabel = labelGenerator(start, local); + + const buckets: TraceHeatMapTimeBucket = [ + { startTimestamp: start, label: typeof firstLabel === 'function' ? firstLabel() : firstLabel }, + ]; + + for (let i = 1; i < bucketCounts; i += 1) { + const lastItem = buckets[buckets.length - 1]; + if (!lastItem) break; + const nextStartTimeStamp = lastItem.startTimestamp + TIME_IN_MS[scale]; + const lbl = labelGenerator(nextStartTimeStamp, local); + buckets.push({ startTimestamp: nextStartTimeStamp, label: typeof lbl === 'function' ? lbl() : lbl }); + } + + return buckets; +}; + +export const MILLISECONDS_TIME_SCALE: TraceHeatMapTimeScale[] = [ + { + scale: 'yearly', + getBuckets: (start: number, end: number, local?: string): TraceHeatMapTimeBucket => { + const generateYearlyLabel = (ts: number): string => { + const begin = new Date(ts).toISOString().split('T')[0]?.split('-')[0] ?? ''; + const end = Number(begin) + 1; + return `${begin}-${end}`; + }; + return produceBuckets('year', generateYearlyLabel, start, end, local); + }, + }, + { + scale: 'monthly', + 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' }); + return (): string => { + return formatter.format(ts); + }; + }; + return produceBuckets('month', generateMonthlyLabel, start, end, local); + }, + }, + { + scale: 'weekly', + getBuckets: (start: number, end: number, local?: string): TraceHeatMapTimeBucket => { + /* optimized using a js closure to avoid creating Intl.DateTimeFormat in every iteration */ + const generateWeeklyLabel = (ts: number) => { + const formatter = new Intl.DateTimeFormat(local, { day: 'numeric', month: 'long' }); + return (): string => { + return `${formatter.format(ts)} - ${formatter.format(ts + TIME_IN_MS.week)}`; + }; + }; + return produceBuckets('week', generateWeeklyLabel, start, end, local); + }, + }, + { + scale: 'daily', + getBuckets: (start: number, end: number, local?: string): TraceHeatMapTimeBucket => { + const generateDailyLabels = (ts: number) => { + const formatter = new Intl.DateTimeFormat(local, { weekday: 'long' }); + return (): string => { + return formatter.format(ts); + }; + }; + return produceBuckets('day', generateDailyLabels, start, end, local); + }, + }, + { + scale: 'hourly', + getBuckets: (start: number, end: number, local?: string): TraceHeatMapTimeBucket => { + const generateHourlyLabels = (ts: number) => { + const formatter = new Intl.DateTimeFormat(local, { hour: 'numeric' }); + return (): string => { + return `${formatter.format(ts)} - ${formatter.format(ts + TIME_IN_MS.hour)}`; + }; + }; + + return produceBuckets('hour', generateHourlyLabels, start, end, local); + }, + }, + { + scale: 'minutes', + getBuckets: (start: number, end: number, local?: string): TraceHeatMapTimeBucket => { + const generateMinuteLabels = (ts: number) => { + const formatter = new Intl.DateTimeFormat(local, { hour: 'numeric', minute: 'numeric', hour12: false }); + return (): string => { + return `${formatter.format(ts)}-${formatter.format(ts + TIME_IN_MS.minute)}`; + }; + }; + return produceBuckets('minute', generateMinuteLabels, start, end, local); + }, + }, + { + scale: 'seconds', + getBuckets: (start: number, end: number, local?: string): TraceHeatMapTimeBucket => { + const generateSecondsLabels = (ts: number) => { + const formatter = new Intl.DateTimeFormat(local, { second: '2-digit' }); + return (): string => { + return `${formatter.format(ts)}-${formatter.format(ts + TIME_IN_MS.second)}`; + }; + }; + return produceBuckets('second', generateSecondsLabels, start, end, local); + }, + }, +]; + +export const getTimeBuckets = ( + resultsStartTimes: Array>, + local?: string +): TraceHeatMapTimeBucket => { + let minStartTime = Infinity; + let maxEndTime = -Infinity; + + for (const result of resultsStartTimes) { + minStartTime = min(minStartTime, result.startTimeUnixMs); + maxEndTime = max(maxEndTime, trunc(result.startTimeUnixMs + result.durationMs)); + } + + if ([minStartTime, maxEndTime].some((t) => !Number.isFinite(t))) { + return []; + } + + const scale = findScale(minStartTime, maxEndTime); + if (!scale) { + return []; + } + const { getBuckets } = MILLISECONDS_TIME_SCALE.find((mts) => mts.scale === scale) || {}; + if (!getBuckets) { + return []; + } + return getBuckets(minStartTime, maxEndTime, local ?? 'en-US'); +}; diff --git a/traceheatmapchart/src/traceheatmap-tooltip-util.test.ts b/traceheatmapchart/src/traceheatmap-tooltip-util.test.ts new file mode 100644 index 000000000..69d73bcdc --- /dev/null +++ b/traceheatmapchart/src/traceheatmap-tooltip-util.test.ts @@ -0,0 +1,206 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { TopLevelFormatterParams } from 'echarts/types/dist/shared'; +import { createBucketToolTip, escapeHtml, ToolTipChartData } from './traceheatmap-tooltip-util'; + +const mockData = { + params: { + value: [0, 5, 2], + }, + toolTipChartData: { + bucketCountMap: [ + [0, 1, 0, 0, 1, 2, 0, 0, 0, 1, 0], + [1, 0, 0, 1, 2, 2, 0, 1, 0, 3, 0], + [0, 0, 0, 1, 2, 0, 1, 1, 0, 0, 0], + ], + bucketToFlatResultsMap: [ + [[], [15], [], [], [17], [16, 19], [], [], [], [18], []], + [[8], [], [], [14], [6, 11], [5, 10], [], [13], [], [7, 9, 12], []], + [[], [], [], [2], [0, 4], [], [3], [1], [], [], []], + ], + durationBuckets: [ + { + start: 8, + label: '8 ms', + }, + { + start: 16, + label: '16 ms', + }, + { + start: 32, + label: '32 ms', + }, + { + start: 64, + label: '64 ms', + }, + { + start: 128, + label: '128 ms', + }, + { + start: 256, + label: '256 ms', + }, + { + start: 512, + label: '512 ms', + }, + { + start: 1024, + label: '1024 ms', + }, + { + start: 2048, + label: '2048 ms', + }, + { + start: 4096, + label: '4096 ms', + }, + { + start: 8192, + label: '8192 ms', + }, + ], + timeBuckets: [ + { + startTimestamp: 1784540414538.0178, + label: '11:40-11:41', + }, + { + startTimestamp: 1784540474538.0178, + label: '11:41-11:42', + }, + { + startTimestamp: 1784540534538.0178, + label: '11:42-11:43', + }, + ], + flatResults: [ + { + startTimeUnixMs: 1784540566155.6975, + durationMs: 150, + }, + { + // eslint-disable-next-line no-loss-of-precision + startTimeUnixMs: 1784540562271.2812, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784540562188.8926, + durationMs: 81, + }, + { + startTimeUnixMs: 1784540540915.67, + durationMs: 900, + }, + { + startTimeUnixMs: 1784540538764.3755, + durationMs: 150, + }, + { + startTimeUnixMs: 1784540533028.299, + durationMs: 400, + }, + { + startTimeUnixMs: 1784540503971.5842, + durationMs: 150, + }, + { + startTimeUnixMs: 1784540498970.7666, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784540498960.0603, + durationMs: 10, + }, + { + startTimeUnixMs: 1784540493838.0383, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784540492564.4324, + durationMs: 400, + }, + { + startTimeUnixMs: 1784540486921.2476, + durationMs: 150, + }, + { + startTimeUnixMs: 1784540481716.7546, + durationMs: 5001, + }, + { + startTimeUnixMs: 1784540479520.5488, + durationMs: 2001, + }, + { + startTimeUnixMs: 1784540479439.648, + durationMs: 80, + }, + { + startTimeUnixMs: 1784540458808.4639, + durationMs: 31, + }, + { + startTimeUnixMs: 1784540432018.206, + durationMs: 400, + }, + { + startTimeUnixMs: 1784540431867.385, + durationMs: 150, + }, + { + startTimeUnixMs: 1784540414938.3296, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784540414538.0178, + durationMs: 400, + }, + ], + }, +}; + +describe('traceheatmap-tooltip-util', () => { + describe('escapeHtml', () => { + [ + { ch: '&', substitute: '&' }, + { ch: '<', substitute: '&alt;' }, + { ch: '>', substitute: '>' }, + { ch: '"', substitute: '"' }, + { ch: "'", substitute: ''' }, + ].forEach((i) => { + it(`should replace ${i.ch} with ${i.substitute}`, () => { + expect(escapeHtml(i.ch)).toBe(i.substitute); + }); + }); + }); + describe('tool-tip', () => { + it('should create a tooltip with all elements', () => { + const { params, toolTipChartData } = mockData; + const toolTip = createBucketToolTip(params as TopLevelFormatterParams, toolTipChartData as ToolTipChartData); + const container = document.createElement('div'); + container.setAttribute('id', 'test-container'); + container.innerHTML = toolTip; + const [column, row] = params.value; + ['time', 'duration', 'traces', 'shared', 'min', 'max'].forEach((element) => { + const id = `bucket-${column}-${row}-tooltip-${element}`; + expect(container.querySelector(`#${id}`)).not.toBeNull(); + }); + }); + }); +}); diff --git a/traceheatmapchart/src/traceheatmap-tooltip-util.ts b/traceheatmapchart/src/traceheatmap-tooltip-util.ts new file mode 100644 index 000000000..c3bdff953 --- /dev/null +++ b/traceheatmapchart/src/traceheatmap-tooltip-util.ts @@ -0,0 +1,142 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* shortened */ +const min = Math.min; +const max = Math.max; + +import { TopLevelFormatterParams } from 'echarts/types/dist/shared'; +import { HeatmapChartData, FlatQueryResults } from './traceheatmap-chart-data-util'; + +export const escapeHtml = (value: string): string => { + return value + .replaceAll('&', '&') + .replaceAll('<', '&alt;') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +}; + +export type ToolTipChartData = Pick< + HeatmapChartData, + 'durationBuckets' | 'timeBuckets' | 'bucketCountMap' | 'bucketToFlatResultsMap' +> & { flatResults: FlatQueryResults }; + +export const createBucketToolTip = (params: TopLevelFormatterParams, toolTipChartData: ToolTipChartData): string => { + if (Array.isArray(params)) { + return ''; + } + + const { bucketCountMap, durationBuckets, timeBuckets, bucketToFlatResultsMap, flatResults } = toolTipChartData; + const [column, row, count] = params.value as [number, number, number]; + const bucketDetails: Array<{ label: string; value: string | undefined; elementId: string }> = []; + + const generateId = (column: number, row: number, element: string): string => + `bucket-${column}-${row}-tooltip-${element}`; + + bucketDetails.push({ + label: 'Time', + value: timeBuckets[column]?.label, + elementId: generateId(column, row, 'time'), + }); + + bucketDetails.push({ + label: 'Duration', + elementId: generateId(column, row, 'duration'), + value: ((): string => { + const begin = durationBuckets[row]; + const end = durationBuckets[row + 1]; + if (end === undefined) { + return `+${begin?.start} ms`; + } + return `${begin?.start}ms-${end.start}ms`; + })(), + }); + + bucketDetails.push({ + label: 'Traces', + value: Number(count) + '', + elementId: generateId(column, row, 'traces'), + }); + + bucketDetails.push({ + label: 'Share of traces', + elementId: generateId(column, row, 'shared'), + value: ((): string | undefined => { + const total = (bucketCountMap[column] || []).reduce((prev, current) => { + return typeof current === 'number' ? prev + current : prev; + }, 0); + const current = bucketCountMap[column]?.[row]; + return total === undefined || current === undefined ? undefined : `${((current / total) * 100).toFixed(2)}%`; + })(), + }); + + bucketDetails.push({ + label: 'Min', + elementId: generateId(column, row, 'min'), + value: ((): string | undefined => { + const indexes = bucketToFlatResultsMap[column]?.[row]; + if (!indexes?.length) return undefined; + + let minimum = Infinity; + for (const idx of indexes) { + const result = flatResults[idx]; + if (result) { + minimum = min(result.durationMs, minimum); + } + } + + return Number.isFinite(minimum) ? `${minimum}` : undefined; + })(), + }); + + bucketDetails.push({ + label: 'Max', + elementId: generateId(column, row, 'max'), + value: ((): string | undefined => { + const indexes = bucketToFlatResultsMap[column]?.[row]; + if (!indexes?.length) return undefined; + + let maximum = -Infinity; + for (const idx of indexes) { + const result = flatResults[idx]; + if (result) { + maximum = max(result.durationMs, maximum); + } + } + + return Number.isFinite(maximum) ? `${maximum}` : undefined; + })(), + }); + + return `
+ ${((): string => { + return bucketDetails + .filter((i) => i.value) + .map( + (i) => `
+ ${escapeHtml(i.label)}: ${escapeHtml(i.value || '')} +
` + ) + .join(''); + })()} +
`; +}; diff --git a/traceheatmapchart/tsconfig.build.json b/traceheatmapchart/tsconfig.build.json new file mode 100644 index 000000000..fc0aafe27 --- /dev/null +++ b/traceheatmapchart/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["**/*.stories.*", "**/*.test.*", "**/*.map"], + "compilerOptions": { + "emitDeclarationOnly": true, + "declaration": true, + "preserveWatchOutput": true + } +} diff --git a/traceheatmapchart/tsconfig.json b/traceheatmapchart/tsconfig.json new file mode 100644 index 000000000..40e3c4dfe --- /dev/null +++ b/traceheatmapchart/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "outDir": "./dist/lib", + "rootDir": "./src", + "target": "es2022", + "lib": ["dom", "dom.iterable", "esnext"], + "module": "esnext", + "jsx": "react-jsx", + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noUncheckedIndexedAccess": true, + "declaration": true, + "declarationMap": true, + "pretty": true + }, + "include": ["src"] +} \ No newline at end of file