E2E: Add performance monitoring E2E test (#109417)

* initial workflow

* add bench reporter

* add pr trigger

* update workflow

* actually add the test

* debug

.....

* more debug

* this it???

* Debugging GHA workflow

https

newline

tree

cache node modules

try bench again

fix path

* Add RequestsRecorder and tweak GHA workflow

* More debugging GHA workflow

cache

cache key

try renaming path

specify env var correctly

* Run tests on both envs, wait for requests to finish

fix status

cat stats

cat stats

* prefix metric names

* cleanup

* cleanup gha

* codeowners

* make gen-jsonnet

* quote things

* Add host_type label for categorising the various hosts

* rename pw_boot_time_seconds to pw_test_run_time_seconds

* change prefix

* codeowners, gha name

* remove node-modules caching

* fix codeowners
This commit is contained in:
Josh Hunt
2025-08-20 19:36:54 +00:00
committed by GitHub
parent 7c2945bb54
commit fbca70747c
10 changed files with 549 additions and 166 deletions
+2
View File
@@ -499,6 +499,7 @@
/e2e-playwright/various-suite/trace-view-scrolling.spec.ts @grafana/observability-traces-and-profiling
/e2e-playwright/various-suite/verify-i18n.spec.ts @grafana/grafana-frontend-platform
/e2e-playwright/various-suite/visualization-suggestions.spec.ts @grafana/dashboards-squad
/e2e-playwright/various-suite/perf-test.spec.ts @grafana/grafana-frontend-platform
# Packages
/packages/README.md @grafana/grafana-frontend-platform
@@ -1253,6 +1254,7 @@ embed.go @grafana/grafana-as-code
/.github/workflows/analytics-events-report.yml @grafana/grafana-frontend-platform
/.github/workflows/pr-e2e-tests.yml @grafana/grafana-developer-enablement-squad
/.github/workflows/skye-add-to-project.yml @grafana/grafana-frontend-platform
/.github/workflows/frontend-perf-tests.yaml @grafana/grafana-frontend-platform
/.github/zizmor.yml @grafana/grafana-developer-enablement-squad
/.github/license_finder.yaml @bergquist
/.github/actionlint.yaml @grafana/grafana-developer-enablement-squad
+133
View File
@@ -0,0 +1,133 @@
name: Frontend performance tests
on:
schedule:
- cron: "0 * * * *" # Run hourly
workflow_dispatch: # Allow manual triggering
jobs:
performance-tests:
permissions:
contents: read
id-token: write
runs-on: ubuntu-x64-large
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
persist-credentials: false
- id: get-secrets
uses: grafana/shared-workflows/actions/get-vault-secrets@62722333225a1fae03ae27a63d638f9bc2176edb #get-vault-secrets-v1.2.0
with:
repo_secrets: |
PROMETHEUS_URL=frontend_perf_tests:prometheus_push_url
PROMETHEUS_USER=frontend_perf_tests:prometheus_user
PROMETHEUS_TOKEN=frontend_perf_tests:prometheus_token
FSPERFBASELINE_USERNAME=frontend_perf_tests:fsperfbaseline_username
FSPERFBASELINE_PASSWORD=frontend_perf_tests:fsperfbaseline_password
FSPERF_USERNAME=frontend_perf_tests:fsperf_username
FSPERF_PASSWORD=frontend_perf_tests:fsperf_password
- name: Authenticate Docker
uses: grafana/shared-workflows/actions/login-to-gar@main
id: login-to-gar
with:
registry: 'us-docker.pkg.dev'
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Yarn install
run: yarn install --immutable
env:
PUPPETEER_SKIP_DOWNLOAD: true
CYPRESS_INSTALL_BINARY: 0
- name: Install Playwright browsers
run: yarn playwright install chromium --with-deps
- name: Run Playwright tests (fsperfbaseline)
id: pw-fsperfbaseline
continue-on-error: true
env:
PLAYWRIGHT_JSON_OUTPUT_NAME: ./fsperfbaseline-pw-results.json
METRICS_OUTPUT_PATH: ./fsperfbaseline-metrics.txt
GRAFANA_URL: https://fsperfbaseline.grafana-dev.net
GRAFANA_ADMIN_USER: ${{ env.FSPERFBASELINE_USERNAME }}
GRAFANA_ADMIN_PASSWORD: ${{ env.FSPERFBASELINE_PASSWORD }}
run: yarn e2e:playwright --grep @performance --reporter json
- name: Run Playwright tests (fsperf)
id: pw-fsperf
continue-on-error: true
env:
PLAYWRIGHT_JSON_OUTPUT_NAME: ./fsperf-pw-results.json
METRICS_OUTPUT_PATH: ./fsperf-metrics.txt
GRAFANA_URL: https://fsperf.grafana-dev.net
GRAFANA_ADMIN_USER: ${{ env.FSPERF_USERNAME }}
GRAFANA_ADMIN_PASSWORD: ${{ env.FSPERF_PASSWORD }}
run: yarn e2e:playwright --grep @performance --reporter json
- name: Bench report (fsperfbaseline)
id: bench-fsperfbaseline
if: always()
continue-on-error: true
run: |
docker run \
--rm \
--volume="./fsperfbaseline-pw-results.json:/pw-results.json" \
--volume="./fsperfbaseline-metrics.txt:/metrics.txt" \
-e PROMETHEUS_URL="$PROMETHEUS_URL" \
-e PROMETHEUS_USER="$PROMETHEUS_USER" \
-e PROMETHEUS_PASSWORD="$PROMETHEUS_TOKEN" \
us-docker.pkg.dev/grafanalabs-global/docker-grafana-bench-prod/grafana-bench:v0.6.0 report \
--grafana-url "http://fsperfbaseline.grafana-dev.net" \
--test-suite-name "FrontendPerfTests" \
--report-input playwright \
--report-output log \
--prometheus-metrics \
--run-metrics-file "/metrics.txt" \
--log-level DEBUG \
/pw-results.json
- name: Bench report (fsperf)
id: bench-fsperf
if: always()
continue-on-error: true
run: |
docker run \
--rm \
--volume="./fsperf-pw-results.json:/pw-results.json" \
--volume="./fsperf-metrics.txt:/metrics.txt" \
-e PROMETHEUS_URL="$PROMETHEUS_URL" \
-e PROMETHEUS_USER="$PROMETHEUS_USER" \
-e PROMETHEUS_PASSWORD="$PROMETHEUS_TOKEN" \
us-docker.pkg.dev/grafanalabs-global/docker-grafana-bench-prod/grafana-bench:v0.6.0 report \
--grafana-url "http://fsperf.grafana-dev.net" \
--test-suite-name "FrontendPerfTests" \
--report-input playwright \
--report-output log \
--prometheus-metrics \
--run-metrics-file "/metrics.txt" \
--log-level DEBUG \
/pw-results.json
- name: Check status
env:
FSPERF_PW_OUTCOME: ${{ steps.pw-fsperf.outcome }}
FSPERF_PW_BASELINE_OUTCOME: ${{ steps.pw-fsperfbaseline.outcome }}
FSPERF_BENCH_OUTCOME: ${{ steps.bench-fsperf.outcome }}
FSPERF_BENCH_BASELINE_OUTCOME: ${{ steps.bench-fsperfbaseline.outcome }}
run: |
echo "FSPERF_PW_OUTCOME: $FSPERF_PW_OUTCOME"
echo "FSPERF_PW_BASELINE_OUTCOME: $FSPERF_PW_BASELINE_OUTCOME"
echo "FSPERF_BENCH_OUTCOME: $FSPERF_BENCH_OUTCOME"
echo "FSPERF_BENCH_BASELINE_OUTCOME: $FSPERF_BENCH_BASELINE_OUTCOME"
if [[ "$FSPERF_PW_OUTCOME" != "success" || "$FSPERF_PW_BASELINE_OUTCOME" != "success" || "$FSPERF_BENCH_OUTCOME" != "success" || "$FSPERF_BENCH_BASELINE_OUTCOME" != "success" ]]; then
echo "One or more test steps failed."
exit 1
fi
@@ -0,0 +1,98 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 10,
"links": [],
"panels": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 10,
"w": 24,
"x": 0,
"y": 0
},
"id": 1,
"maxDataPoints": 10,
"options": {
"counters": {
"dataChanged": true,
"render": true,
"schemaChanged": true
},
"mode": "events",
"stateView": ""
},
"pluginVersion": "12.2.0-16975765900",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"refId": "A"
}
],
"title": "Panel Title",
"type": "debug"
}
],
"preload": false,
"refresh": "",
"schemaVersion": 41,
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "Mostly blank dashboard",
"uid": "bds35fot3cv7kb",
"version": 3
}
+1
View File
@@ -69,6 +69,7 @@
"loki_fakedata": (import '../dev-dashboards/datasource-loki/loki_fakedata.json'),
"loki_query_splitting": (import '../dev-dashboards/datasource-loki/loki_query_splitting.json'),
"migrations": (import '../dev-dashboards/migrations/migrations.json'),
"mostly-blank-dashboard": (import '../dev-dashboards/scenarios/mostly-blank-dashboard.json'),
"mssql_fakedata": (import '../dev-dashboards/datasource-mssql/mssql_fakedata.json'),
"mssql_unittest": (import '../dev-dashboards/datasource-mssql/mssql_unittest.json'),
"mysql_fakedata": (import '../dev-dashboards/datasource-mysql/mysql_fakedata.json'),
+164
View File
@@ -0,0 +1,164 @@
import { Page, Response, Request } from '@playwright/test';
import * as prom from 'prom-client';
/**
* Records and tracks network request body sizes.
*
* You must call listen() before page.goto() for accurate results - this ensures all responses
* for a page are tracked.
*/
export class RequestsRecorder {
#page: Page;
#documentUrl: string | undefined;
#requestsInFlight = 0;
#currentRequests: Set<Request> = new Set<Request>();
#inflatedSizeBytesCounter: prom.Counter<'type' | 'host_type'>;
#transferSizeBytesCounter: prom.Counter<'type' | 'host_type'>;
#requestCountCounter: prom.Counter<'type' | 'host_type'>;
#resolve?: () => void;
constructor(page: Page) {
this.#page = page;
this.#inflatedSizeBytesCounter = new prom.Counter({
name: 'fe_perf_inflated_size_bytes',
help: 'The size of the inflated response body in bytes',
labelNames: ['type', 'host_type'],
registers: [],
});
this.#transferSizeBytesCounter = new prom.Counter({
name: 'fe_perf_transfer_size_bytes',
help: 'The size of the transfered response body in bytes',
labelNames: ['type', 'host_type'],
registers: [],
});
this.#requestCountCounter = new prom.Counter({
name: 'fe_perf_request_count',
help: 'The number of requests made',
labelNames: ['type', 'host_type'],
registers: [],
});
}
listen() {
const handler = this.#handleResponse.bind(this);
const reqHandler = this.#handleRequest.bind(this);
this.#page.on('request', reqHandler);
this.#page.on('response', handler);
return () => {
this.#page.off('response', handler);
this.#page.off('request', reqHandler);
if (this.#requestsInFlight === 0) {
return Promise.resolve();
}
console.log('waiting for', this.#requestsInFlight, 'requests to finish');
return new Promise<void>((resolve) => {
this.#resolve = resolve;
});
};
}
async #handleRequest(request: Request) {
const type = request.resourceType();
// Once we've recieved a document response, create a list of requests that we'll count the responses of.
// We also want to count the document request itself.
if (this.#documentUrl || type === 'document') {
this.#currentRequests.add(request);
}
}
/*
* The Playwright page object has have multiple page loads, and responses for one page may come in after the page
* has been navigated away from. Attempting to get the body of these responses will results in an error, but is also
* not what we want to track.
*
* To solve this, we wait for the 'document' response to come in (a new page has loaded) and then keep track of all
* future requests. We only record responses for requests that were made after the document response.
*/
async #handleResponse(response: Response) {
const request = response.request();
const url = response.url();
const type = response.request().resourceType();
// Record when a document response comes in so we can keep track of future requests
if (type === 'document') {
if (this.#documentUrl) {
console.warn('recieved additional document response', url);
}
this.#documentUrl = url;
}
// Disregard responses that for requests that were initiated before the current page
if (!this.#currentRequests.has(request)) {
return;
}
this.#requestsInFlight += 1;
const hostType = getHostType(response.url(), this.#documentUrl ?? '');
// Attempting to get the body of an empty response results in an error, so guess if the response will be empty or not
const statusCode = response.status();
const noBodyStatusCode = statusCode <= 199 || statusCode === 204 || statusCode === 205 || statusCode === 304;
if (!noBodyStatusCode) {
const body = await response.body();
this.#inflatedSizeBytesCounter.inc({ type, host_type: hostType }, body.length);
}
const sizes = await response.request().sizes();
this.#transferSizeBytesCounter.inc(
{ type, host_type: hostType },
sizes.responseBodySize + sizes.responseHeadersSize
);
this.#requestCountCounter.inc({ type, host_type: hostType });
this.#requestsInFlight -= 1;
if (this.#requestsInFlight === 0 && this.#resolve) {
this.#resolve();
}
}
getMetrics() {
return [this.#inflatedSizeBytesCounter, this.#transferSizeBytesCounter, this.#requestCountCounter];
}
}
/**
* Instead of setting the request host as a label, which may have too high cardinality, we categorise
* the host into a limited set of types.
*/
function getHostType(requestUrl: string, documentUrl: string) {
const url = new URL(requestUrl);
const hostname = url.hostname; // FYI `hostname` doesn't include port, `host` does
const documentHost = new URL(documentUrl).hostname;
if (hostname.match(/^grafana-assets\.grafana(-\w+)?\.net$/)) {
return 'assets_cdn';
}
if (hostname.match(/^plugins-cdn\.grafana(-\w+)?\.net$/)) {
return 'plugin_cdn';
}
if (hostname === documentHost) {
return 'self';
}
return 'other';
}
@@ -0,0 +1,58 @@
import fs from 'fs';
import * as prom from 'prom-client';
import { test, expect } from '@grafana/plugin-e2e';
import { RequestsRecorder } from '../utils/RequestsRecorder';
const DASH_PATH = '/d/bds35fot3cv7kb/mostly-blank-dashboard';
test('payload-size', { tag: '@performance' }, async ({ page }) => {
const promRegistry = new prom.Registry();
const testRunTimeGauge = new prom.Gauge({
name: 'fe_perf_test_run_time_seconds',
help: 'The time it took for the performance test to run',
registers: [promRegistry],
});
const usedJSHeapSizeGauge = new prom.Gauge({
name: 'fe_perf_used_js_heap_size_bytes',
help: 'The amount of memory used by the JavaScript heap',
registers: [promRegistry],
});
const recorder = new RequestsRecorder(page);
const stopListening = recorder.listen();
let start = performance.now();
await page.goto(DASH_PATH);
let el = page.getByTestId('data-testid header-container');
await el.waitFor();
await expect(el).toBeVisible();
let end = performance.now();
let client = await page.context().newCDPSession(page);
await client.send('HeapProfiler.collectGarbage');
let usedJSHeapSize = (await client.send('Runtime.getHeapUsage')).usedSize;
const responseMetrics = recorder.getMetrics();
for (const metric of responseMetrics) {
promRegistry.registerMetric(metric);
}
testRunTimeGauge.set(Math.round(end - start) / 1000);
usedJSHeapSizeGauge.set(+usedJSHeapSize.toFixed(1));
const instance = new URL(process.env.GRAFANA_URL || 'http://undefined').host;
promRegistry.setDefaultLabels({ instance });
const metricsText = await promRegistry.metrics();
console.log(metricsText);
fs.writeFileSync(process.env.METRICS_OUTPUT_PATH || '/tmp/asset-metrics.txt', metricsText);
await stopListening();
page.close();
});
+1
View File
@@ -234,6 +234,7 @@
"postcss-reporter": "7.1.0",
"postcss-scss": "4.0.9",
"prettier": "3.6.2",
"prom-client": "^15.1.3",
"publint": "^0.3.12",
"react-refresh": "0.14.0",
"react-select-event": "5.5.1",
+61 -165
View File
@@ -1,4 +1,4 @@
import { defineConfig, devices } from '@playwright/test';
import { defineConfig, devices, PlaywrightTestConfig, Project } from '@playwright/test';
import path, { dirname } from 'path';
import { PluginOptions } from '@grafana/plugin-e2e';
@@ -7,6 +7,19 @@ const testDirRoot = 'e2e-playwright';
const pluginDirRoot = path.join(testDirRoot, 'plugin-e2e');
const DEFAULT_URL = 'http://localhost:3001';
function withAuth(project: Project): Project {
project.dependencies ??= [];
project.use ??= {};
project.dependencies = project.dependencies.concat('authenticate');
project.use = {
...project.use,
storageState: `playwright/.auth/${process.env.GRAFANA_ADMIN_USER || 'admin'}.json`,
};
return project;
}
export default defineConfig<PluginOptions>({
fullyParallel: true,
/* Retry on CI only */
@@ -20,6 +33,7 @@ export default defineConfig<PluginOptions>({
timeout: 10_000,
},
use: {
...devices['Desktop Chrome'],
baseURL: process.env.GRAFANA_URL ?? DEFAULT_URL,
trace: 'retain-on-failure',
httpCredentials: {
@@ -59,15 +73,10 @@ export default defineConfig<PluginOptions>({
},
},
// Run all tests in parallel using user with admin role
{
withAuth({
name: 'admin',
testDir: path.join(pluginDirRoot, '/plugin-e2e-api-tests/as-admin-user'),
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['authenticate'],
},
}),
// Run all tests in parallel using user with viewer role
{
name: 'viewer',
@@ -78,210 +87,97 @@ export default defineConfig<PluginOptions>({
},
dependencies: ['createUserAndAuthenticate'],
},
{
withAuth({
name: 'elasticsearch',
testDir: path.join(pluginDirRoot, '/elasticsearch'),
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['authenticate'],
},
{
}),
withAuth({
name: 'mysql',
testDir: path.join(pluginDirRoot, '/mysql'),
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['authenticate'],
},
{
}),
withAuth({
name: 'mssql',
testDir: path.join(pluginDirRoot, '/mssql'),
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['authenticate'],
},
{
}),
withAuth({
name: 'extensions-test-app',
testDir: path.join(testDirRoot, '/test-plugins/grafana-extensionstest-app'),
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['authenticate'],
},
{
}),
withAuth({
name: 'grafana-e2etest-datasource',
testDir: path.join(testDirRoot, '/test-plugins/grafana-test-datasource'),
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['authenticate'],
},
{
}),
withAuth({
name: 'cloudwatch',
testDir: path.join(pluginDirRoot, '/cloudwatch'),
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['authenticate'],
},
{
}),
withAuth({
name: 'azuremonitor',
testDir: path.join(pluginDirRoot, '/azuremonitor'),
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['authenticate'],
},
{
}),
withAuth({
name: 'cloudmonitoring',
testDir: path.join(pluginDirRoot, '/cloudmonitoring'),
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['authenticate'],
},
{
}),
withAuth({
name: 'graphite',
testDir: path.join(pluginDirRoot, '/graphite'),
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['authenticate'],
},
{
}),
withAuth({
name: 'influxdb',
testDir: path.join(pluginDirRoot, '/influxdb'),
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['authenticate'],
},
{
}),
withAuth({
name: 'opentsdb',
testDir: path.join(pluginDirRoot, '/opentsdb'),
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['authenticate'],
},
{
}),
withAuth({
name: 'jaeger',
testDir: path.join(pluginDirRoot, '/jaeger'),
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['authenticate'],
},
{
}),
withAuth({
name: 'grafana-postgresql-datasource',
testDir: path.join(pluginDirRoot, '/grafana-postgresql-datasource'),
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['authenticate'],
},
{
}),
withAuth({
name: 'canvas',
testDir: path.join(testDirRoot, '/canvas'),
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['authenticate'],
},
{
}),
withAuth({
name: 'zipkin',
testDir: path.join(pluginDirRoot, '/zipkin'),
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['authenticate'],
},
}),
{
name: 'unauthenticated',
testDir: path.join(testDirRoot, '/unauthenticated'),
use: {
...devices['Desktop Chrome'],
},
},
{
withAuth({
name: 'various',
testDir: path.join(testDirRoot, '/various-suite'),
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['authenticate'],
},
{
}),
withAuth({
name: 'panels',
testDir: path.join(testDirRoot, '/panels-suite'),
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['authenticate'],
},
{
}),
withAuth({
name: 'smoke',
testDir: path.join(testDirRoot, '/smoke-tests-suite'),
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['authenticate'],
},
{
}),
withAuth({
name: 'dashboards',
testDir: path.join(testDirRoot, '/dashboards-suite'),
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['authenticate'],
},
{
}),
withAuth({
name: 'loki',
testDir: path.join(testDirRoot, '/loki'),
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['authenticate'],
},
{
}),
withAuth({
name: 'cloud-plugins',
testDir: path.join(testDirRoot, '/cloud-plugins-suite'),
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['authenticate'],
},
{
}),
withAuth({
name: 'dashboard-new-layouts',
testDir: path.join(testDirRoot, '/dashboard-new-layouts'),
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['authenticate'],
},
}),
],
});
+3
View File
@@ -18,6 +18,9 @@ queryService=true
[environment]
stack_id = 12345
[panels]
enable_alpha = true
[plugins]
allow_loading_unsigned_plugins=grafana-extensionstest-app,grafana-extensionexample1-app,grafana-extensionexample2-app,grafana-extensionexample3-app,grafana-e2etest-datasource
+28 -1
View File
@@ -5851,7 +5851,7 @@ __metadata:
languageName: node
linkType: hard
"@opentelemetry/api@npm:1.9.0, @opentelemetry/api@npm:^1.3.0, @opentelemetry/api@npm:^1.9.0":
"@opentelemetry/api@npm:1.9.0, @opentelemetry/api@npm:^1.3.0, @opentelemetry/api@npm:^1.4.0, @opentelemetry/api@npm:^1.9.0":
version: 1.9.0
resolution: "@opentelemetry/api@npm:1.9.0"
checksum: 10/a607f0eef971893c4f2ee2a4c2069aade6ec3e84e2a1f5c2aac19f65c5d9eeea41aa72db917c1029faafdd71789a1a040bdc18f40d63690e22ccae5d7070f194
@@ -12201,6 +12201,13 @@ __metadata:
languageName: node
linkType: hard
"bintrees@npm:1.0.2":
version: 1.0.2
resolution: "bintrees@npm:1.0.2"
checksum: 10/071896cea5ea5413316c8436e95799444c208630d5c539edd8a7089fc272fc5d3634aa4a2e4847b28350dda1796162e14a34a0eda53108cc5b3c2ff6a036c1fa
languageName: node
linkType: hard
"bl@npm:^4.0.3, bl@npm:^4.1.0":
version: 4.1.0
resolution: "bl@npm:4.1.0"
@@ -18542,6 +18549,7 @@ __metadata:
postcss-scss: "npm:4.0.9"
prettier: "npm:3.6.2"
prismjs: "npm:1.30.0"
prom-client: "npm:^15.1.3"
publint: "npm:^0.3.12"
rc-slider: "npm:11.1.8"
rc-tree: "npm:5.13.1"
@@ -26297,6 +26305,16 @@ __metadata:
languageName: node
linkType: hard
"prom-client@npm:^15.1.3":
version: 15.1.3
resolution: "prom-client@npm:15.1.3"
dependencies:
"@opentelemetry/api": "npm:^1.4.0"
tdigest: "npm:^0.1.1"
checksum: 10/eba75e15ab896845d39359e3a4d6f7913ea05339b3122d8dde8c8c374669ad1a1d1ab2694ab2101c420bd98086a564e4f2a18aa29018fc14a4732e57c1c19aec
languageName: node
linkType: hard
"promise-all-reject-late@npm:^1.0.0":
version: 1.0.1
resolution: "promise-all-reject-late@npm:1.0.1"
@@ -30724,6 +30742,15 @@ __metadata:
languageName: node
linkType: hard
"tdigest@npm:^0.1.1":
version: 0.1.2
resolution: "tdigest@npm:0.1.2"
dependencies:
bintrees: "npm:1.0.2"
checksum: 10/45be99fa52dab74b8edafe150e473cdc45aa1352c75ed516a39905f350a08c3175f6555598111042c3677ba042d7e3cae6b5ce4c663fe609bc634f326aabc9d6
languageName: node
linkType: hard
"teex@npm:^1.0.1":
version: 1.0.1
resolution: "teex@npm:1.0.1"