diff --git a/.changeset/config.json b/.changeset/config.json index 0aad346e6..496e542e8 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -10,5 +10,5 @@ "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": ["@salesforce/b2c-plugin-example-config"] + "ignore": ["@salesforce/b2c-plugin-example-config", "b2c-grafana-datasource"] } diff --git a/.changeset/grafana-datasource-and-go-sdk.md b/.changeset/grafana-datasource-and-go-sdk.md new file mode 100644 index 000000000..4efe3816f --- /dev/null +++ b/.changeset/grafana-datasource-and-go-sdk.md @@ -0,0 +1,5 @@ +--- +'@salesforce/b2c-tooling-sdk': minor +--- + +Metrics tag enrichment (`parseSeriesTags`/`enrichMetricsTags`) is now driven by a declarative rule catalog exported to `specs/metrics-tags-catalog.json`, with a golden fixture (`specs/metrics-tags.golden.json`) that pins the expected tag output. This is the shared source of truth consumed by the new Go SDK and Grafana datasource, and guards against parser drift. No API changes — existing callers are unaffected. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15c489fac..9c79df64f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -280,3 +280,46 @@ jobs: packages/b2c-tooling-sdk/coverage/ packages/b2c-cli/coverage/ retention-days: 30 + + test-go: + runs-on: ubuntu-latest + + strategy: + matrix: + go-version: ['1.26.x'] + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup Go ${{ matrix.go-version }} + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + cache-dependency-path: | + packages/b2c-tooling-sdk-go/go.sum + packages/b2c-grafana-datasource/go.sum + + - name: Go SDK - Build + working-directory: packages/b2c-tooling-sdk-go + run: go build ./... + + - name: Go SDK - Vet + working-directory: packages/b2c-tooling-sdk-go + run: go vet ./... + + - name: Go SDK - Test + working-directory: packages/b2c-tooling-sdk-go + run: go test ./... -v + + - name: Grafana Plugin - Build + working-directory: packages/b2c-grafana-datasource + run: go build ./... + + - name: Grafana Plugin - Vet + working-directory: packages/b2c-grafana-datasource + run: go vet ./... + + - name: Grafana Plugin - Test + working-directory: packages/b2c-grafana-datasource + run: go test ./... -v diff --git a/AGENTS.md b/AGENTS.md index 9ec50893f..708d33920 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,6 +5,8 @@ This is a monorepo project with the following packages: - `./packages/b2c-tooling-sdk` - the SDK/library for B2C Commerce operations; supports the CLI and can be used standalone - `./packages/b2c-dx-mcp` - Model Context Protocol server; also built with oclif - `./packages/b2c-vs-extension` - VS Code extension (not published to npm; packaged as VSIX and versioned via git tags) +- `./packages/b2c-grafana-datasource` - Grafana datasource plugins (Metrics + CIP) with Go backend; versioned via git tags +- `./packages/b2c-tooling-sdk-go` - Go SDK for Metrics API and CIP; standalone, used by Grafana plugins; versioned via git tags - `./docs` - documentation site (private `@salesforce/b2c-dx-docs` workspace package; not published to npm) ## Common Commands @@ -26,6 +28,29 @@ pnpm --filter @salesforce/b2c-cli run dev ./cli ``` +## Grafana Plugin / Go SDK Development + +The Grafana plugin and Go SDK are separate from the Node.js/TypeScript packages: + +```bash +# Build Grafana plugin (both datasources) +cd packages/b2c-grafana-datasource +npm run build # Frontend + both backend binaries +go build -o dist/gpx_b2c_metrics ./pkg +go build -o dist/gpx_b2c_cip ./pkg/cip + +# Test Go SDK +cd packages/b2c-tooling-sdk-go +go test ./... +go test -cover ./... + +# Run Grafana demo (Docker-based) +cd packages/b2c-grafana-datasource +make demo # Mock data, no credentials +make real INSTANCE=bdpx-prd # Live B2C tenant via b2c CLI +make down # Stop demo +``` + ## Commands for Coding Agents These commands produce condensed output optimized for AI coding agents: diff --git a/packages/b2c-grafana-datasource/.config/README.md b/packages/b2c-grafana-datasource/.config/README.md new file mode 100644 index 000000000..ecab64e61 --- /dev/null +++ b/packages/b2c-grafana-datasource/.config/README.md @@ -0,0 +1,57 @@ +# Webpack Build Configuration + +This directory contains the webpack configuration for building the Grafana datasource frontend plugin. + +## Files + +- **webpack.config.ts** - Main webpack configuration that produces AMD module format for Grafana + +## Build Output + +The webpack build produces the following in `dist/`: +- `module.js` - AMD module loadable by Grafana's SystemJS runtime +- `module.js.map` - Source map for debugging +- `plugin.json` - Plugin metadata (copied from src/) +- `img/logo.svg` - Plugin logo (copied from src/img/) + +## Key Configuration Details + +### AMD Module Format +Grafana uses SystemJS to load plugins, which requires AMD module format: +```typescript +output: { + libraryTarget: 'amd' +} +``` + +### Externals +These packages are provided by Grafana at runtime and must not be bundled: +- `@grafana/data`, `@grafana/ui`, `@grafana/runtime` +- `react`, `react-dom` +- `@emotion/*` (used internally by Grafana) +- `lodash` + +### Build Tool: SWC +Uses `swc-loader` instead of `ts-loader` for faster TypeScript compilation. + +## Commands + +```bash +# Production build +npm run build + +# Development build with watch mode +npm run dev + +# Type check only +npm run typecheck +``` + +## Docker Integration + +The Docker build stage runs: +```dockerfile +RUN npm install --production=false && npm run build +``` + +This produces a complete plugin directory ready to mount into Grafana. diff --git a/packages/b2c-grafana-datasource/.config/webpack.config.ts b/packages/b2c-grafana-datasource/.config/webpack.config.ts new file mode 100644 index 000000000..5b2b434fb --- /dev/null +++ b/packages/b2c-grafana-datasource/.config/webpack.config.ts @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import CopyWebpackPlugin from 'copy-webpack-plugin'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { Configuration } from 'webpack'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const config = (env: any): Configuration => { + const isProduction = env.production === true; + + return { + mode: isProduction ? 'production' : 'development', + target: 'web', + + entry: { + // Metrics datasource → dist/module.js + module: './src/module.ts', + // CIP datasource → dist/cip/module.js (separate plugin dir) + 'cip/module': './src/cip/module.ts', + }, + + output: { + path: path.resolve(__dirname, '../dist'), + filename: '[name].js', + libraryTarget: 'amd', // Grafana uses AMD/SystemJS module format + clean: true, // Clean dist folder before build + }, + + // Externals - Grafana provides these at runtime + externals: [ + 'lodash', + 'react', + 'react-dom', + '@grafana/data', + '@grafana/ui', + '@grafana/runtime', + ({ request }, callback) => { + // Externalize all @grafana/* packages + if (request && request.startsWith('@grafana/')) { + return callback(null, request); + } + // Externalize all @emotion/* packages (used by @grafana/ui) + if (request && request.startsWith('@emotion/')) { + return callback(null, request); + } + callback(); + }, + ], + + resolve: { + extensions: ['.ts', '.tsx', '.js', '.jsx'], + // Grafana 10+ uses ESM, but we still need to resolve node modules + alias: { + // Ensure we're using the same React instance as Grafana + react: path.resolve(__dirname, '../node_modules/react'), + 'react-dom': path.resolve(__dirname, '../node_modules/react-dom'), + }, + }, + + module: { + rules: [ + { + test: /\.tsx?$/, + exclude: /node_modules/, + use: { + loader: 'swc-loader', + options: { + jsc: { + parser: { + syntax: 'typescript', + tsx: true, + }, + target: 'es2020', + transform: { + react: { + runtime: 'automatic', + development: !isProduction, + }, + }, + }, + }, + }, + }, + { + test: /\.css$/, + use: ['style-loader', 'css-loader'], + }, + { + test: /\.(png|jpe?g|gif|svg)$/i, + type: 'asset/resource', + }, + ], + }, + + plugins: [ + new CopyWebpackPlugin({ + patterns: [ + { from: 'src/plugin.json', to: '.' }, + { from: 'src/img', to: 'img', noErrorOnMissing: true }, + // CIP datasource assets → dist/cip/ + { from: 'src/cip/plugin.json', to: 'cip' }, + { from: 'src/cip/img', to: 'cip/img', noErrorOnMissing: true }, + ], + }), + ], + + devtool: isProduction ? 'source-map' : 'eval-source-map', + + performance: { + hints: false, + }, + }; +}; + +export default config; diff --git a/packages/b2c-grafana-datasource/.dockerignore b/packages/b2c-grafana-datasource/.dockerignore new file mode 100644 index 000000000..5857df570 --- /dev/null +++ b/packages/b2c-grafana-datasource/.dockerignore @@ -0,0 +1,21 @@ +# Exclude everything except what we need +* + +# Allow the two packages we need +!b2c-grafana-datasource/ +!b2c-tooling-sdk-go/ + +# Exclude build artifacts and dependencies +b2c-grafana-datasource/node_modules/ +b2c-grafana-datasource/dist/ +b2c-grafana-datasource/.git/ +b2c-tooling-sdk-go/.git/ + +# Exclude other packages (if building from packages/ dir) +b2c-cli/ +b2c-tooling-sdk/ +b2c-dx-mcp/ +b2c-vs-extension/ +b2c-agent-plugins/ +mrt-utilities/ +.git/ diff --git a/packages/b2c-grafana-datasource/.eslintrc.json b/packages/b2c-grafana-datasource/.eslintrc.json new file mode 100644 index 000000000..f47d61748 --- /dev/null +++ b/packages/b2c-grafana-datasource/.eslintrc.json @@ -0,0 +1,11 @@ +{ + "extends": ["@grafana/eslint-config"], + "root": true, + "parserOptions": { + "ecmaVersion": 2020, + "sourceType": "module" + }, + "rules": { + "react/prop-types": "off" + } +} diff --git a/packages/b2c-grafana-datasource/.gitignore b/packages/b2c-grafana-datasource/.gitignore new file mode 100644 index 000000000..5cf27c448 --- /dev/null +++ b/packages/b2c-grafana-datasource/.gitignore @@ -0,0 +1,24 @@ +# Build outputs +dist/ + +# Go binaries +*.exe +*.dll +*.so +*.dylib +*.test +*.out +go.work + +# Demo binaries +demo/mock-metrics/mock-metrics + +# Node.js +node_modules/ +*.log +*.boot.log +.DS_Store + +# IDE +.vscode/ +.idea/ diff --git a/packages/b2c-grafana-datasource/Dockerfile b/packages/b2c-grafana-datasource/Dockerfile new file mode 100644 index 000000000..7acb9dfcc --- /dev/null +++ b/packages/b2c-grafana-datasource/Dockerfile @@ -0,0 +1,91 @@ +# Multi-stage Dockerfile for B2C Commerce Metrics Grafana plugin +# Builds both frontend (React/TypeScript) and backend (Go) in one image + +# ============================================================================ +# Stage 1: Build frontend (module.js + plugin.json + logo) +# ============================================================================ +FROM node:22-alpine AS frontend-builder + +WORKDIR /build + +# Copy package files first for layer caching +COPY b2c-grafana-datasource/package*.json ./ + +# Install dependencies (including devDependencies for build) +RUN npm install + +# Copy frontend source files +COPY b2c-grafana-datasource/.config/ ./.config/ +COPY b2c-grafana-datasource/src/ ./src/ +COPY b2c-grafana-datasource/tsconfig.json ./ +COPY b2c-grafana-datasource/.eslintrc.json ./ + +# Build frontend (outputs to dist/) +RUN npm run build + +# ============================================================================ +# Stage 2: Build Go backend for multiple architectures +# ============================================================================ +FROM golang:1.26-alpine AS backend-builder + +# Install build dependencies +RUN apk add --no-cache git + +WORKDIR /go/src/github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages + +# Copy both Go modules (plugin + SDK) +COPY b2c-tooling-sdk-go/ ./b2c-tooling-sdk-go/ +COPY b2c-grafana-datasource/go.mod b2c-grafana-datasource/go.sum ./b2c-grafana-datasource/ +COPY b2c-grafana-datasource/pkg/ ./b2c-grafana-datasource/pkg/ + +# Download Go dependencies +WORKDIR /go/src/github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-grafana-datasource +RUN go mod download + +# Build Metrics backend (linux amd64 + arm64) +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /gpx_b2c_metrics_linux_amd64 ./pkg +RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o /gpx_b2c_metrics_linux_arm64 ./pkg + +# Build CIP backend (linux amd64 + arm64) +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /gpx_b2c_cip_linux_amd64 ./pkg/cip +RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o /gpx_b2c_cip_linux_arm64 ./pkg/cip + +# ============================================================================ +# Stage 3: Assemble final Grafana image with BOTH datasource plugins +# ============================================================================ +FROM grafana/grafana:11.2.0 + +# Allow both unsigned plugins to load +ENV GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS=salesforce-b2c-metrics-datasource,salesforce-b2c-cip-datasource + +# Anonymous admin for demo (disable in production) +ENV GF_AUTH_ANONYMOUS_ENABLED=true \ + GF_AUTH_ANONYMOUS_ORG_ROLE=Admin \ + GF_SECURITY_ADMIN_USER=admin \ + GF_SECURITY_ADMIN_PASSWORD=admin + +USER root + +# --- Metrics plugin dir --- +RUN mkdir -p /var/lib/grafana/plugins/salesforce-b2c-metrics-datasource +COPY --from=frontend-builder /build/dist/module.js /var/lib/grafana/plugins/salesforce-b2c-metrics-datasource/ +COPY --from=frontend-builder /build/dist/plugin.json /var/lib/grafana/plugins/salesforce-b2c-metrics-datasource/ +COPY --from=frontend-builder /build/dist/img/ /var/lib/grafana/plugins/salesforce-b2c-metrics-datasource/img/ +COPY --from=backend-builder /gpx_b2c_metrics_linux_amd64 /var/lib/grafana/plugins/salesforce-b2c-metrics-datasource/ +COPY --from=backend-builder /gpx_b2c_metrics_linux_arm64 /var/lib/grafana/plugins/salesforce-b2c-metrics-datasource/ + +# --- CIP plugin dir --- +RUN mkdir -p /var/lib/grafana/plugins/salesforce-b2c-cip-datasource +COPY --from=frontend-builder /build/dist/cip/module.js /var/lib/grafana/plugins/salesforce-b2c-cip-datasource/ +COPY --from=frontend-builder /build/dist/cip/plugin.json /var/lib/grafana/plugins/salesforce-b2c-cip-datasource/ +COPY --from=frontend-builder /build/dist/cip/img/ /var/lib/grafana/plugins/salesforce-b2c-cip-datasource/img/ +COPY --from=backend-builder /gpx_b2c_cip_linux_amd64 /var/lib/grafana/plugins/salesforce-b2c-cip-datasource/ +COPY --from=backend-builder /gpx_b2c_cip_linux_arm64 /var/lib/grafana/plugins/salesforce-b2c-cip-datasource/ + +RUN chmod +x /var/lib/grafana/plugins/salesforce-b2c-metrics-datasource/gpx_b2c_metrics_linux_* \ + && chmod +x /var/lib/grafana/plugins/salesforce-b2c-cip-datasource/gpx_b2c_cip_linux_* + +USER grafana + +EXPOSE 3000 +CMD ["/run.sh"] diff --git a/packages/b2c-grafana-datasource/Magefile.go b/packages/b2c-grafana-datasource/Magefile.go new file mode 100644 index 000000000..8bd7427b1 --- /dev/null +++ b/packages/b2c-grafana-datasource/Magefile.go @@ -0,0 +1,63 @@ +//go:build mage +// +build mage + +package main + +import ( + "github.com/magefile/mage/sh" +) + +// Build builds the plugin for the current platform. +func Build() error { + return sh.RunV("go", "build", "-o", "dist/gpx_b2c_metrics", "./pkg") +} + +// BuildAll builds the plugin for all supported platforms. +func BuildAll() error { + platforms := []struct { + os string + arch string + }{ + {"linux", "amd64"}, + {"linux", "arm64"}, + {"darwin", "amd64"}, + {"darwin", "arm64"}, + {"windows", "amd64"}, + } + + // Build both metrics and CIP datasources + packages := []struct { + path string + binary string + }{ + {"./pkg", "gpx_b2c_metrics"}, + {"./pkg/cip", "gpx_b2c_cip"}, + } + + for _, pkg := range packages { + for _, p := range platforms { + output := "dist/" + pkg.binary + "_" + p.os + "_" + p.arch + if p.os == "windows" { + output += ".exe" + } + + if err := sh.RunWithV(map[string]string{ + "GOOS": p.os, + "GOARCH": p.arch, + }, "go", "build", "-o", output, pkg.path); err != nil { + return err + } + } + } + return nil +} + +// Clean removes build artifacts. +func Clean() error { + return sh.Rm("dist") +} + +// Test runs the test suite. +func Test() error { + return sh.RunV("go", "test", "-v", "./...") +} diff --git a/packages/b2c-grafana-datasource/Makefile b/packages/b2c-grafana-datasource/Makefile new file mode 100644 index 000000000..56f7b2718 --- /dev/null +++ b/packages/b2c-grafana-datasource/Makefile @@ -0,0 +1,95 @@ +.PHONY: demo real real-env down logs clean build-plugin build-frontend build-backend help + +# Default target +.DEFAULT_GOAL := help + +# B2C CLI wrapper (resolves credentials from dw.json + OS keychain). Override if +# your checkout lives elsewhere: `make real CLI=/path/to/cli`. +CLI ?= ../../cli +# Instance name (from the b2c CLI config) to pull real credentials for. +INSTANCE ?= bdpx-prd + +# ============================================================================ +# Docker Compose Commands +# ============================================================================ + +demo: ## Start Grafana + mock-metrics with synthetic data (no credentials needed) + docker compose up --build + +real: ## Start Grafana against the LIVE Metrics API for $(INSTANCE) (creds via b2c CLI keychain) + @echo "Resolving credentials for instance '$(INSTANCE)' via the b2c CLI (keychain/password-store)..." + @creds=$$($(CLI) setup inspect -i $(INSTANCE) --json --unmask 2>/dev/null) || { echo "ERROR: could not run '$(CLI) setup inspect -i $(INSTANCE) --json --unmask'"; exit 1; }; \ + export B2C_CLIENT_ID=$$(printf '%s' "$$creds" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{console.log(JSON.parse(s).config.clientId||"")}catch{console.log("")}})'); \ + export B2C_CLIENT_SECRET=$$(printf '%s' "$$creds" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{console.log(JSON.parse(s).config.clientSecret||"")}catch{console.log("")}})'); \ + export B2C_SHORT_CODE=$$(printf '%s' "$$creds" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{console.log(JSON.parse(s).config.shortCode||"")}catch{console.log("")}})'); \ + export B2C_TENANT_ID=$$(printf '%s' "$$creds" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{console.log(JSON.parse(s).config.tenantId||"")}catch{console.log("")}})'); \ + export B2C_ACCOUNT_MANAGER_HOST=$${B2C_ACCOUNT_MANAGER_HOST:-account.demandware.com}; \ + if [ -z "$$B2C_CLIENT_ID" ] || [ -z "$$B2C_CLIENT_SECRET" ] || [ -z "$$B2C_SHORT_CODE" ] || [ -z "$$B2C_TENANT_ID" ]; then \ + echo "ERROR: missing resolved credentials for '$(INSTANCE)' (clientId/clientSecret/shortCode/tenantId). Check '$(CLI) setup inspect -i $(INSTANCE)'."; exit 1; fi; \ + echo "Using shortCode=$$B2C_SHORT_CODE tenantId=$$B2C_TENANT_ID clientId=$${B2C_CLIENT_ID%%-*}… (secret hidden)"; \ + docker compose -f docker-compose.yml -f docker-compose.real.yml up -d --build grafana + +down: ## Stop and remove all containers (both modes) + docker compose -f docker-compose.yml -f docker-compose.real.yml down 2>/dev/null || docker compose down + +logs: ## Follow logs from all containers + docker compose logs -f + +clean: ## Stop containers and remove volumes + docker compose -f docker-compose.yml -f docker-compose.real.yml down -v 2>/dev/null || docker compose down -v + +# ============================================================================ +# Local Build Commands (without Docker) +# ============================================================================ + +build-plugin: build-frontend build-backend ## Build both frontend and backend locally + +build-frontend: ## Build frontend (module.js + plugin.json + logo) + @echo "Building frontend..." + npm install + npm run build + @echo "Frontend artifacts in dist/" + +build-backend: ## Build Go backend binaries for linux (amd64 + arm64) + @echo "Building metrics backend for linux/amd64..." + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o dist/gpx_b2c_metrics_linux_amd64 ./pkg + @echo "Building metrics backend for linux/arm64..." + CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o dist/gpx_b2c_metrics_linux_arm64 ./pkg + @echo "Building CIP backend for linux/amd64..." + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o dist/gpx_b2c_cip_linux_amd64 ./pkg/cip + @echo "Building CIP backend for linux/arm64..." + CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o dist/gpx_b2c_cip_linux_arm64 ./pkg/cip + @echo "Backend binaries in dist/" + +# ============================================================================ +# Development Commands +# ============================================================================ + +dev-frontend: ## Build frontend in watch mode + npm run dev + +test-backend: ## Run Go backend tests + go test -v ./... + +lint-frontend: ## Lint frontend code + npm run lint + +typecheck: ## Run TypeScript type checking + npm run typecheck + +# ============================================================================ +# Help +# ============================================================================ + +help: ## Show this help message + @echo "B2C Commerce Metrics Grafana Plugin - Makefile" + @echo "" + @echo "Usage: make " + @echo "" + @echo "Targets:" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-20s %s\n", $$1, $$2}' + @echo "" + @echo "Quick start:" + @echo " make demo # Start demo environment" + @echo " make logs # View logs" + @echo " make down # Stop demo" diff --git a/packages/b2c-grafana-datasource/README.md b/packages/b2c-grafana-datasource/README.md new file mode 100644 index 000000000..536487f98 --- /dev/null +++ b/packages/b2c-grafana-datasource/README.md @@ -0,0 +1,218 @@ +# Salesforce B2C Commerce Datasources for Grafana + +Grafana datasource plugins for B2C Commerce observability — visualize metrics and query analytics data. + +## Two Datasources + +This package provides two independent Grafana datasources: + +### 1. B2C Commerce Metrics + +Time-series metrics from the **Metrics API** (CLOSED BETA): + +- **9 Metric Categories**: Overall, Sales, eCDN, Third-party, SCAPI, SCAPI Hooks, MRT, Controller, OCAPI +- **Auto-enriched Labels**: Realm, environment, API family, host, cache status, status class +- **30-day Retention**: Automatic time range clamping +- **OAuth2 Authentication**: Client credentials flow with token caching + +**Plugin ID**: `salesforce-b2c-metrics-datasource` + +### 2. B2C Commerce Intelligence (CIP) + +Raw SQL queries against the **CIP analytics warehouse**: + +- **Calcite SQL Dialect**: Standard SQL with Grafana time macros +- **Schema Browser**: Explore tables and columns +- **Time Macros**: `$__timeFilter`, `$__timeGroup`, `$__interval` +- **13-month Retention**: Typical for fact tables (varies by table) + +**Plugin ID**: `salesforce-b2c-cip-datasource` + +Both datasources use OAuth2 client credentials and support multi-tenant configurations. + +## Quick Start + +Get started in 5 minutes with Docker: + +```bash +# Demo mode (mock data, no credentials required) +make demo + +# Open http://localhost:3000 (no login) +# Dashboard pre-configured with sample visualizations + +# Real mode (connect to live B2C tenant via b2c CLI) +make real INSTANCE=bdpx-prd +``` + +See [Quick Start Guide](./docs/quickstart.md) for details. + +## Prerequisites + +- **Grafana**: 9.0 or later +- **B2C Commerce**: API credentials with appropriate scopes: + - Metrics: `sfcc.metrics` scope (CLOSED BETA access required) + - CIP: CIP query scope + +## Installation + +### From Source + +```bash +# Build both datasource binaries +cd packages/b2c-grafana-datasource +go mod tidy +go build -o dist/gpx_b2c_metrics ./pkg +go build -o dist/gpx_b2c_cip ./pkg/cip + +# Or use npm scripts +npm run build:backend # Builds both binaries +``` + +### Install to Grafana + +```bash +# Copy plugin to Grafana plugins directory +cp -r . /var/lib/grafana/plugins/b2c-commerce-grafana/ + +# Restart Grafana +sudo systemctl restart grafana-server +``` + +### Allow Unsigned Plugins + +Both plugins are currently unsigned. Add to `grafana.ini`: + +```ini +[plugins] +allow_loading_unsigned_plugins = salesforce-b2c-metrics-datasource,salesforce-b2c-cip-datasource +``` + +See [Quick Start Guide](./docs/quickstart.md) for Docker-based setup. + +## Documentation + +- **[Quick Start](./docs/quickstart.md)**: 5-minute Docker setup (demo + real mode) +- **[Configuration](./docs/configuration.md)**: Datasource settings, OAuth, multi-tenant +- **[Query Editor](./docs/query-editor.md)**: Metrics filters + CIP SQL/macros +- **[API Reference](./docs/api-reference.md)**: CallResource endpoints +- **[Architecture](./docs/architecture.md)**: Technical design + backend contract + +## Example Use Cases + +### Metrics Datasource + +**Monitor SCAPI Product API Performance**: +- Category: `scapi` +- API Family: `product` +- Metrics: `totalCalls`, `p95Latency`, `cacheHitRate` + +**Track eCDN Error Rate by PoP**: +- Category: `ecdn` +- Label Filter: `statusClass=~5xx` +- Group By: `host` + +**OCAPI Shop Endpoint Latency**: +- Category: `ocapi` +- OCAPI Category: `shop` +- Metrics: `p95Latency` + +### CIP Datasource + +**Orders Over Time**: +```sql +SELECT + $__timeGroupAlias(submit_date, 1h), + COUNT(*) as orders, + SUM(revenue) as revenue +FROM orders +WHERE $__timeFilter(submit_date) +GROUP BY $__timeGroup(submit_date, 1h) +ORDER BY 1 +``` + +**Top Products by Revenue**: +```sql +SELECT + p.name, + SUM(li.price * li.quantity) as revenue +FROM order_line_items li + JOIN products p ON li.product_id = p.product_id +WHERE $__timeFilter(li.submit_date) +GROUP BY p.name +ORDER BY revenue DESC +LIMIT 10 +``` + +## Development + +### Build + +```bash +# Backend (Go) +go build -o dist/gpx_b2c_metrics ./pkg +go build -o dist/gpx_b2c_cip ./pkg/cip + +# Frontend (React + TypeScript) +npm install +npm run build + +# Both +npm run build:all +``` + +### Test + +```bash +# Go tests +go test -v ./... + +# Demo environment +make demo +``` + +### Cross-compile + +```bash +# Linux amd64 +GOOS=linux GOARCH=amd64 go build -o dist/gpx_b2c_metrics_linux_amd64 ./pkg +GOOS=linux GOARCH=amd64 go build -o dist/gpx_b2c_cip_linux_amd64 ./pkg/cip + +# Linux arm64 +GOOS=linux GOARCH=arm64 go build -o dist/gpx_b2c_metrics_linux_arm64 ./pkg +GOOS=linux GOARCH=arm64 go build -o dist/gpx_b2c_cip_linux_arm64 ./pkg/cip +``` + +## Architecture + +The plugins use a **Go backend + React frontend** architecture: + +- **Backend**: Go binaries implementing Grafana's plugin SDK + - Metrics: `pkg/plugin/` (hand-rolled QueryData) + - CIP: `pkg/cip/plugin/` (uses grafana/sqlds/v4 SQL driver framework) + - Shared SDK: `../b2c-tooling-sdk-go` (OAuth, clients, operations) + +- **Frontend**: React + TypeScript query editors + - Metrics: `src/` (tiered filters, label filters, group-by) + - CIP: `src/cip/` (SQL editor, schema browser, macros) + +Communication via JSON-RPC over HTTP/gRPC. See [Architecture Guide](./docs/architecture.md) for details. + +## Related Packages + +- **[b2c-tooling-sdk-go](../b2c-tooling-sdk-go/)**: Standalone Go SDK for Metrics API and CIP +- **[b2c-tooling-sdk](../b2c-tooling-sdk/)**: TypeScript SDK (CLI + MCP) +- **[b2c-cli](../b2c-cli/)**: Command-line interface +- **[b2c-dx-mcp](../b2c-dx-mcp/)**: Model Context Protocol server + +## License + +Copyright (c) 2025, Salesforce, Inc. Licensed under Apache-2.0. + +See [license.txt](../../license.txt) in repository root. + +## Support + +- **Issues**: https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/issues +- **Documentation**: https://developer.salesforce.com/docs/commerce/commerce-api +- **B2C CLI**: Related tooling at https://github.com/SalesforceCommerceCloud/b2c-developer-tooling diff --git a/packages/b2c-grafana-datasource/RESILIENCE_PLAN.md b/packages/b2c-grafana-datasource/RESILIENCE_PLAN.md new file mode 100644 index 000000000..f8b4f12a7 --- /dev/null +++ b/packages/b2c-grafana-datasource/RESILIENCE_PLAN.md @@ -0,0 +1,1090 @@ +# B2C Grafana Plugin — Rate-Limit & Caching Resilience Plan + +_Generated from a research+audit+verify workflow (2026-07-14). OSS-only; Enterprise features flagged._ + +1. **Define `HTTPError` type** in new file `/Users/clavery/code/b2c-developer-tooling/feature-grafana/packages/b2c-tooling-sdk-go/clients/metrics/errors.go`: + + ```go + package metrics + + import ( + "fmt" + "net/http" + "strconv" + "time" + ) + + type HTTPError struct { + StatusCode int + Body string + RetryAfter time.Duration // 0 if no Retry-After header + } + + func (e *HTTPError) Error() string { + if e.RetryAfter > 0 { + return fmt.Sprintf("HTTP %d: %s (retry after %s)", e.StatusCode, e.Body, e.RetryAfter) + } + return fmt.Sprintf("HTTP %d: %s", e.StatusCode, e.Body) + } + + func (e *HTTPError) IsRetryable() bool { + return e.StatusCode == http.StatusTooManyRequests || // 429 + (e.StatusCode >= 500 && e.StatusCode < 600) || // 5xx + e.StatusCode == http.StatusRequestTimeout // 408 + } + + func (e *HTTPError) IsPermanent() bool { + switch e.StatusCode { + case http.StatusBadRequest, // 400 + http.StatusUnauthorized, // 401 + http.StatusForbidden, // 403 + http.StatusNotFound: // 404 + return true + } + return false + } + + // ParseRetryAfter extracts Retry-After header (seconds or HTTP-date). + func ParseRetryAfter(header string) time.Duration { + if header == "" { + return 0 + } + // Try integer seconds first + if seconds, err := strconv.Atoi(header); err == nil && seconds > 0 { + return time.Duration(seconds) * time.Second + } + // Try HTTP-date (RFC 7231 sec 7.1.3) + if t, err := http.ParseTime(header); err == nil { + return time.Until(t) + } + return 0 + } + ``` + +2. **Update `getMetrics` method** in `/Users/clavery/code/b2c-developer-tooling/feature-grafana/packages/b2c-tooling-sdk-go/clients/metrics/client.go` (line ~139): + + ```go + // Replace: + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("metrics API error (status %d): %s", resp.StatusCode, string(body)) + } + + // With: + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + retryAfter := ParseRetryAfter(resp.Header.Get("Retry-After")) + return nil, &HTTPError{ + StatusCode: resp.StatusCode, + Body: string(body), + RetryAfter: retryAfter, + } + } + ``` + +**Files changed:** +- New: `/Users/clavery/code/b2c-developer-tooling/feature-grafana/packages/b2c-tooling-sdk-go/clients/metrics/errors.go` +- Modified: `/Users/clavery/code/b2c-developer-tooling/feature-grafana/packages/b2c-tooling-sdk-go/clients/metrics/client.go` (`getMetrics` method) + +**Effort:** 1–2 hours + +--- + +### 3.2 Retry with Backoff (Metrics Client) + +**What:** Wrap `getMetrics` in `cenkalti/backoff/v4.RetryWithData` with exponential backoff + Retry-After honor. + +**Why:** Transient errors (429, 5xx, timeouts) should retry automatically; current code fails immediately. + +**Implementation:** + +1. **Add retry wrapper** in `client.go` (new helper function): + + ```go + import ( + "github.com/cenkalti/backoff/v4" + ) + + func (c *Client) getMetricsWithRetry(ctx context.Context, category string, window metricsops.ResolvedMetricsWindow, filters map[string]string) (*MetricsDataResponse, error) { + // Configure backoff + b := backoff.NewExponentialBackOff() + b.InitialInterval = 1 * time.Second + b.MaxInterval = 30 * time.Second + b.MaxElapsedTime = 2 * time.Minute + bCtx := backoff.WithContext(b, ctx) + + operation := func() (*MetricsDataResponse, error) { + resp, err := c.getMetrics(ctx, category, window, filters) + + if err == nil { + return resp, nil // success + } + + var httpErr *HTTPError + if errors.As(err, &httpErr) { + if httpErr.IsPermanent() { + return nil, backoff.Permanent(err) // stop immediately + } + if httpErr.IsRetryable() { + // Honor Retry-After by sleeping before returning error + if httpErr.RetryAfter > 0 { + select { + case <-time.After(httpErr.RetryAfter): + case <-ctx.Done(): + return nil, backoff.Permanent(ctx.Err()) + } + } + return nil, err // retry with backoff + } + return nil, backoff.Permanent(err) // unknown status, don't retry + } + + // Network errors (timeout, connection refused) → retry + return nil, err + } + + return backoff.RetryWithData(operation, bCtx) + } + ``` + +2. **Update public methods** (`GetOrderMetrics`, `GetSiteMetrics`, `GetSystemMetrics`) to call `getMetricsWithRetry` instead of `getMetrics`. + +**Files changed:** +- `/Users/clavery/code/b2c-developer-tooling/feature-grafana/packages/b2c-tooling-sdk-go/clients/metrics/client.go` (add `getMetricsWithRetry`, update `Get*Metrics`) + +**Effort:** 2–3 hours + +--- + +### 3.3 Error Source + Status Mapping (Datasource Backend) + +**What:** Map `HTTPError` → `backend.ErrDataResponseWithSource(backend.StatusTooManyRequests, backend.ErrorSourceDownstream, msg)` in QueryData. + +**Why:** Current code uses `backend.ErrDataResponse(backend.StatusInternal, ...)` for ALL errors—Grafana treats 429s as plugin bugs, not API rate limits. + +**Implementation:** + +1. **Add error mapping helper** in `/Users/clavery/code/b2c-developer-tooling/feature-grafana/packages/b2c-grafana-datasource/pkg/plugin/datasource.go`: + + ```go + import ( + "errors" + "github.com/grafana/grafana-plugin-sdk-go/backend" + metricsClient "github.com/salesforce/b2c-tooling-sdk-go/clients/metrics" + ) + + func mapMetricsErrorToDataResponse(err error) backend.DataResponse { + var httpErr *metricsClient.HTTPError + if errors.As(err, &httpErr) { + var status backend.Status + switch httpErr.StatusCode { + case 429: + status = backend.StatusTooManyRequests + case 400: + status = backend.StatusBadRequest + case 401: + status = backend.StatusUnauthorized + case 403: + status = backend.StatusForbidden + case 404: + status = backend.StatusNotFound + case 408: + status = backend.StatusTimeout + case 502, 503, 504: + status = backend.StatusBadGateway + default: + if httpErr.StatusCode >= 500 { + status = backend.StatusBadGateway + } else { + status = backend.StatusInternal + } + } + + source := backend.ErrorSourceFromHTTPStatus(httpErr.StatusCode) + return backend.ErrDataResponseWithSource(status, source, httpErr.Error()) + } + + // Non-HTTP errors (network, context cancellation) + return backend.ErrDataResponseWithSource( + backend.StatusInternal, + backend.ErrorSourceDownstream, + err.Error(), + ) + } + ``` + +2. **Update QueryData error handling** (line ~223): + + ```go + // Replace: + response.Responses[qm.RefID] = backend.ErrDataResponse( + backend.StatusInternal, + fmt.Sprintf("metrics API error: %v", err), + ) + + // With: + response.Responses[qm.RefID] = mapMetricsErrorToDataResponse(err) + ``` + +**Files changed:** +- `/Users/clavery/code/b2c-developer-tooling/feature-grafana/packages/b2c-grafana-datasource/pkg/plugin/datasource.go` (add `mapMetricsErrorToDataResponse`, update QueryData lines 198-234) + +**Effort:** 1–2 hours + +--- + +### 3.4 Discovery Probe Caching (Highest ROI) + +**What:** Cache `probeCategory` results (5–15min TTL) keyed by `(datasourceUID, category, tenantId)`. + +**Why:** Current code fires live 24-hour probes on EVERY query editor interaction (`/metrics`, `/label-keys`, `/label-values`). A single user editing a dashboard can trigger 50+ probes in 2 minutes. + +**Implementation:** + +1. **Add cache to datasource struct** (line ~31 in `datasource.go`): + + ```go + import ( + gocache "github.com/patrickmn/go-cache" + ) + + type MetricsDatasource struct { + clientCache map[string]*metrics.Client + cacheLock sync.RWMutex + probeCache *gocache.Cache // NEW + settings backend.DataSourceInstanceSettings + } + + func newMetricsDatasource(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + return &MetricsDatasource{ + clientCache: make(map[string]*metrics.Client), + probeCache: gocache.New(10*time.Minute, 15*time.Minute), // 10min TTL, 15min cleanup + settings: settings, + }, nil + } + ``` + +2. **Wrap `probeCategory` with cache** (new helper function): + + ```go + func (d *MetricsDatasource) probeCategoryWithCache(ctx context.Context, category string, client *metrics.Client, tenantID string) (*metricsops.CategoryProbe, error) { + // Cache key: datasourceUID + category + tenantID + cacheKey := fmt.Sprintf("%s:%s:%s", d.settings.UID, category, tenantID) + + // Check cache + if cached, found := d.probeCache.Get(cacheKey); found { + return cached.(*metricsops.CategoryProbe), nil + } + + // Cache miss — probe API + probe, err := probeCategory(ctx, category, client) + if err != nil { + // On error, return stale cache if available (graceful degradation) + if cached, found := d.probeCache.Get(cacheKey); found { + backend.Logger.Warn("probe failed, returning stale cache", "category", category, "error", err) + return cached.(*metricsops.CategoryProbe), nil + } + return nil, err + } + + // Cache successful probe + d.probeCache.Set(cacheKey, probe, gocache.DefaultExpiration) + return probe, nil + } + ``` + +3. **Update CallResource handlers** (lines 618, 620, 622): + + ```go + // In handleGetMetrics, handleLabelKeys, handleLabelValues: + // Replace: + probe, err := probeCategory(ctx, category, client) + + // With: + probe, err := d.probeCategoryWithCache(ctx, category, client, tenantID) + ``` + +**Files changed:** +- `/Users/clavery/code/b2c-developer-tooling/feature-grafana/packages/b2c-grafana-datasource/pkg/plugin/datasource.go` (add `probeCache` field, `probeCategoryWithCache`, update CallResource handlers) + +**Dependencies:** Add `github.com/patrickmn/go-cache` to `go.mod`: + +```bash +cd /Users/clavery/code/b2c-developer-tooling/feature-grafana/packages/b2c-grafana-datasource +go get github.com/patrickmn/go-cache@v2.1.0+incompatible +``` + +**Effort:** 3–4 hours + +**Expected impact:** 80–95% reduction in discovery API calls (from N calls per editor interaction to 1 call per 10min per category). + +--- + +### 3.5 CheckHealth Retry + Cache Fallback + +**What:** Wrap health probe in retry; cache last successful result (5min TTL); return cached OK if recent success. + +**Why:** Health checks fire on every Save & Test. If a transient 429 occurs, datasource appears broken in UI. + +**Implementation:** + +1. **Add health cache field** to datasource struct: + + ```go + type MetricsDatasource struct { + clientCache map[string]*metrics.Client + cacheLock sync.RWMutex + probeCache *gocache.Cache + healthCache *gocache.Cache // NEW (separate cache for health results) + settings backend.DataSourceInstanceSettings + } + + func newMetricsDatasource(...) { + return &MetricsDatasource{ + clientCache: make(map[string]*metrics.Client), + probeCache: gocache.New(10*time.Minute, 15*time.Minute), + healthCache: gocache.New(5*time.Minute, 10*time.Minute), // 5min TTL + settings: settings, + }, nil + } + ``` + +2. **Update `CheckHealth` method** (line ~537): + + ```go + func (d *MetricsDatasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + cacheKey := fmt.Sprintf("health:%s", d.settings.UID) + + // Try health probe with retry + var lastErr error + for attempt := 0; attempt < 3; attempt++ { + client, err := d.getMetricsClient(ctx, req.PluginContext) + if err != nil { + lastErr = err + time.Sleep(time.Duration(attempt+1) * time.Second) // simple backoff + continue + } + + // Probe with 5min window (lightweight check) + window, _ := metricsops.ResolveMetricsWindow( + metricsops.MetricsWindowInput{Window: "5m"}, + time.Now(), + ) + _, err = client.GetSystemMetrics(ctx, window, nil) + + if err == nil { + // Success — cache and return + result := &backend.CheckHealthResult{ + Status: backend.HealthStatusOk, + Message: "Metrics API is reachable", + } + d.healthCache.Set(cacheKey, result, gocache.DefaultExpiration) + return result, nil + } + + var httpErr *metrics.HTTPError + if errors.As(err, &httpErr) && !httpErr.IsRetryable() { + // Permanent error (401, 403) — fail immediately + return &backend.CheckHealthResult{ + Status: backend.HealthStatusError, + Message: fmt.Sprintf("Authentication failed: %v", err), + }, nil + } + + lastErr = err + time.Sleep(time.Duration(attempt+1) * time.Second) + } + + // All retries failed — check cache for graceful degradation + if cached, found := d.healthCache.Get(cacheKey); found { + backend.Logger.Warn("health probe failed, returning cached OK", "error", lastErr) + return cached.(*backend.CheckHealthResult), nil + } + + // No cache, all retries failed + return &backend.CheckHealthResult{ + Status: backend.HealthStatusError, + Message: fmt.Sprintf("Health check failed after retries: %v", lastErr), + }, nil + } + ``` + +**Files changed:** +- `/Users/clavery/code/b2c-developer-tooling/feature-grafana/packages/b2c-grafana-datasource/pkg/plugin/datasource.go` (add `healthCache`, update `CheckHealth`) + +**Effort:** 1–2 hours + +--- + +## 4. Tier 2 — Strong Wins (Post-Tier-1) + +### 4.1 QueryData Response Caching + +**What:** TTL cache keyed by `hash(category, filters, roundedFrom, roundedTo, tenantId)`. + +**Why:** Dashboard auto-refresh fires identical queries every N seconds. Cache avoids redundant API calls. + +**Implementation:** + +1. **Add query cache field**: + + ```go + type MetricsDatasource struct { + clientCache map[string]*metrics.Client + cacheLock sync.RWMutex + probeCache *gocache.Cache + healthCache *gocache.Cache + queryCache *gocache.Cache // NEW + settings backend.DataSourceInstanceSettings + } + + func newMetricsDatasource(...) { + return &MetricsDatasource{ + // ... + queryCache: gocache.New(2*time.Minute, 5*time.Minute), // 2min TTL + // ... + } + } + ``` + +2. **Add cache key builder**: + + ```go + import "crypto/sha256" + + func buildQueryCacheKey(datasourceUID, category string, from, to time.Time, filters map[string]string, tenantID string) string { + // Round time range to 1min buckets for better cache hit rate + roundedFrom := from.Truncate(1 * time.Minute) + roundedTo := to.Truncate(1 * time.Minute) + + // Serialize filters (sorted keys for stability) + keys := make([]string, 0, len(filters)) + for k := range filters { + keys = append(keys, k) + } + sort.Strings(keys) + var filterStr string + for _, k := range keys { + filterStr += fmt.Sprintf("%s=%s;", k, filters[k]) + } + + raw := fmt.Sprintf("%s:%s:%d:%d:%s:%s", + datasourceUID, category, roundedFrom.Unix(), roundedTo.Unix(), filterStr, tenantID) + hash := sha256.Sum256([]byte(raw)) + return fmt.Sprintf("%x", hash[:16]) // first 16 bytes + } + ``` + +3. **Wrap GetXMetrics calls** in QueryData (lines 198-217): + + ```go + cacheKey := buildQueryCacheKey(d.settings.UID, qm.Category, from, to, qm.Filters, tenantID) + + // Check cache + if cached, found := d.queryCache.Get(cacheKey); found { + response.Responses[qm.RefID] = cached.(backend.DataResponse) + continue + } + + // Cache miss — fetch from API + var dataResponse *metrics.MetricsDataResponse + var err error + switch qm.Category { + case "orders": + dataResponse, err = client.GetOrderMetrics(ctx, window, qm.Filters) + // ... other categories + } + + if err != nil { + errResp := mapMetricsErrorToDataResponse(err) + response.Responses[qm.RefID] = errResp + // Do NOT cache errors (only successful responses) + continue + } + + frames := dataResponse.ToFrames() + dataResp := backend.DataResponse{Frames: frames} + response.Responses[qm.RefID] = dataResp + + // Cache successful response + d.queryCache.Set(cacheKey, dataResp, gocache.DefaultExpiration) + ``` + +**Files changed:** +- `/Users/clavery/code/b2c-developer-tooling/feature-grafana/packages/b2c-grafana-datasource/pkg/plugin/datasource.go` (add `queryCache`, `buildQueryCacheKey`, update QueryData) + +**Effort:** 4–6 hours + +**Expected impact:** 50–80% reduction in QueryData API calls for dashboards with refresh intervals shorter than cache TTL. + +--- + +### 4.2 Singleflight Request Coalescing + +**What:** Use `golang.org/x/sync/singleflight.Group` to collapse concurrent identical panel queries. + +**Why:** Dashboard refresh fires all panel queries concurrently. If 5 panels query the same data, singleflight reduces 5 calls → 1 call. + +**Implementation:** + +1. **Add singleflight group**: + + ```go + import "golang.org/x/sync/singleflight" + + type MetricsDatasource struct { + clientCache map[string]*metrics.Client + cacheLock sync.RWMutex + probeCache *gocache.Cache + healthCache *gocache.Cache + queryCache *gocache.Cache + singleflightGroup singleflight.Group // NEW + settings backend.DataSourceInstanceSettings + } + ``` + +2. **Wrap GetXMetrics with singleflight**: + + ```go + sfKey := buildQueryCacheKey(d.settings.UID, qm.Category, from, to, qm.Filters, tenantID) + + result, err, shared := d.singleflightGroup.Do(sfKey, func() (interface{}, error) { + // Check cache inside singleflight (cache-aside pattern) + if cached, found := d.queryCache.Get(sfKey); found { + return cached.(backend.DataResponse), nil + } + + // Cache miss — fetch from API + var dataResponse *metrics.MetricsDataResponse + var apiErr error + switch qm.Category { + case "orders": + dataResponse, apiErr = client.GetOrderMetrics(ctx, window, qm.Filters) + // ... + } + + if apiErr != nil { + return mapMetricsErrorToDataResponse(apiErr), nil + } + + frames := dataResponse.ToFrames() + dataResp := backend.DataResponse{Frames: frames} + d.queryCache.Set(sfKey, dataResp, gocache.DefaultExpiration) + return dataResp, nil + }) + + if err != nil { + response.Responses[qm.RefID] = mapMetricsErrorToDataResponse(err) + } else { + response.Responses[qm.RefID] = result.(backend.DataResponse) + if shared { + backend.Logger.Debug("query coalesced via singleflight", "refID", qm.RefID) + } + } + ``` + +**Files changed:** +- `/Users/clavery/code/b2c-developer-tooling/feature-grafana/packages/b2c-grafana-datasource/pkg/plugin/datasource.go` (add `singleflightGroup`, wrap QueryData calls) + +**Dependencies:** `golang.org/x/sync` (already present as transitive dep). + +**Effort:** 2–3 hours + +**Expected impact:** 50–90% reduction in concurrent duplicate queries (dashboard refresh with identical panel queries). + +--- + +### 4.3 CIP Schema Caching + +**What:** Cache `ListTables`, `DescribeColumns`, `Sites` results (15–30min TTL). + +**Why:** Query editor dropdowns call these on every refresh; schema changes rarely. + +**Implementation:** + +1. **Add schema cache to CIP datasource**: + + ```go + // In /Users/clavery/code/b2c-developer-tooling/feature-grafana/packages/b2c-grafana-datasource/pkg/cip/plugin/datasource.go + + type CIPDatasource struct { + clientCache map[string]*cip.Client + cacheLock sync.RWMutex + schemaCache *gocache.Cache // NEW + settings backend.DataSourceInstanceSettings + } + + func newCIPDatasource(...) { + return &CIPDatasource{ + clientCache: make(map[string]*cip.Client), + schemaCache: gocache.New(20*time.Minute, 30*time.Minute), // 20min TTL + settings: settings, + }, nil + } + ``` + +2. **Wrap CallResource handlers** (lines 209, 215, 222): + + ```go + func (d *CIPDatasource) handleListTables(ctx context.Context, client *cip.Client, tenantID string) ([]byte, error) { + cacheKey := fmt.Sprintf("tables:%s:%s", d.settings.UID, tenantID) + + if cached, found := d.schemaCache.Get(cacheKey); found { + return cached.([]byte), nil + } + + tables, err := client.ListTables(ctx) + if err != nil { + return nil, err + } + + data, _ := json.Marshal(tables) + d.schemaCache.Set(cacheKey, data, gocache.DefaultExpiration) + return data, nil + } + + // Similar for handleDescribeColumns, handleListSites + ``` + +**Files changed:** +- `/Users/clavery/code/b2c-developer-tooling/feature-grafana/packages/b2c-grafana-datasource/pkg/cip/plugin/datasource.go` (add `schemaCache`, wrap CallResource handlers) + +**Effort:** 2–3 hours + +--- + +### 4.4 Dashboard Refresh Guidance (Documentation) + +**What:** Document min refresh interval, maxDataPoints impact, provide rate-limit-friendly templates. + +**Where:** `/Users/clavery/code/b2c-developer-tooling/feature-grafana/packages/b2c-grafana-datasource/README.md` + docs site. + +**Content:** + +```markdown +## Rate Limit Best Practices + +The Salesforce B2C Commerce APIs have tight rate limits. Follow these guidelines: + +1. **Min Refresh Interval:** Set dashboard auto-refresh to **5–10 minutes minimum**. + - High-frequency refreshes (< 1min) will hit rate limits quickly. + - For real-time monitoring, use Grafana Enterprise streaming or increase cache TTL. + +2. **Max Data Points:** Keep default (1000) unless you need higher resolution. + - Higher maxDataPoints → more frequent API calls (wider time ranges get coarser intervals). + +3. **Query Time Ranges:** Use relative time ranges (e.g., "Last 1 hour", "Last 24 hours") instead of absolute. + - Relative ranges with rounded timestamps improve cache hit rates. + +4. **Panel Queries:** Avoid duplicate queries across panels. + - Use dashboard variables for shared filters (site, tenant). + - Leverage panel transformations instead of separate queries. + +5. **Discovery Probes:** Query editor dropdowns are cached (10min TTL). + - If dropdowns appear stale, wait or refresh datasource config. + +6. **429 Rate Limit Errors:** Orange "Rate limited" badge on panel. + - Plugin automatically retries with exponential backoff (up to 2min). + - If persistent, reduce dashboard refresh rate or narrow time window. +``` + +**Effort:** 1–2 hours + +--- + +## 5. Tier 3 — Nice to Have / Enterprise + +- **Grafana Enterprise Query Caching** (admin-configured, server-side): NOT available in OSS. If using Enterprise/Cloud, enable via datasource settings UI (cache TTL, per-query cache keys). +- **Streaming / Incremental Updates**: Grafana Live for real-time data push. Requires backend streaming support (WebSocket/SSE). Not currently supported by B2C Metrics API. +- **Connection Pooling Tuning**: CIP already maxed at `MaxOpenConns=1` due to sticky-session requirement (x-session-id affinity). Metrics client uses default `http.Client` connection pool (100 conns max, 90s idle timeout). + +--- + +## 6. CIP vs Metrics Differences + +| Aspect | Metrics (HTTP/REST) | CIP (database/sql + Avatica) | +|--------|---------------------|------------------------------| +| **Error Detection** | Parse `resp.StatusCode`; extract Retry-After header | Errors opaque (no HTTP status); detect timeout/connection by text matching (`strings.Contains(err.Error(), "timeout")`) | +| **Retry Middleware** | Can use `http.RoundTripper` middleware for transport-level retry | Must retry at `Query`/`Exec` call site; wrap `db.QueryContext` in backoff loop | +| **Retry Config** | `InitialInterval=1s`, `MaxElapsedTime=2min`, retry 429/5xx/timeout | `InitialInterval=500ms`, `MaxElapsedTime=30s` (lower—synchronous SQL); retry timeout/connection errors only | +| **Caching** | Cache QueryData responses (time-series) + discovery probes (categories/labels) | Cache schema only (ListTables, DescribeColumns, Sites); do NOT cache QueryData (user-defined WHERE clauses vary) | +| **Sticky Session** | N/A (stateless REST) | `x-session-id` echo transport required; `MaxOpenConns=1` ensures one conn per backend; retry must preserve session OR re-establish | +| **Error Source** | Map `HTTPError.StatusCode` → `backend.StatusTooManyRequests` + `ErrorSourceDownstream` | All errors → `backend.StatusInternal` + `ErrorSourceDownstream` (can't extract HTTP status); classify by error text | + +### CIP-Specific Retry Implementation + +```go +// In /Users/clavery/code/b2c-developer-tooling/feature-grafana/packages/b2c-tooling-sdk-go/clients/cip/client.go + +func (c *Client) QueryWithRetry(ctx context.Context, query string) (*sql.Rows, error) { + b := backoff.NewExponentialBackOff() + b.InitialInterval = 500 * time.Millisecond + b.MaxInterval = 10 * time.Second + b.MaxElapsedTime = 30 * time.Second + bCtx := backoff.WithContext(b, ctx) + + operation := func() (*sql.Rows, error) { + rows, err := c.db.QueryContext(ctx, query) + if err == nil { + return rows, nil + } + + errStr := err.Error() + // Detect retryable errors (timeout, connection) + if strings.Contains(errStr, "timeout") || + strings.Contains(errStr, "connection") || + strings.Contains(errStr, "i/o timeout") { + return nil, err // retry + } + + // Detect permanent errors (auth, SQL syntax) + if strings.Contains(errStr, "unauthorized") || + strings.Contains(errStr, "forbidden") || + strings.Contains(errStr, "syntax error") { + return nil, backoff.Permanent(err) + } + + // Unknown error — don't retry + return nil, backoff.Permanent(err) + } + + return backoff.RetryWithData(operation, bCtx) +} +``` + +**Files changed:** +- `/Users/clavery/code/b2c-developer-tooling/feature-grafana/packages/b2c-tooling-sdk-go/clients/cip/client.go` (add `QueryWithRetry`, update `Query`/`ListTables`/`DescribeColumns` to call it) + +**Effort:** 2–3 hours + +--- + +## 7. Recommended Shared Abstraction + +**Where:** New package `/Users/clavery/code/b2c-developer-tooling/feature-grafana/packages/b2c-tooling-sdk-go/resilience` + +**Why:** Both Metrics and CIP need retry+backoff; discovery and schema endpoints need caching. Centralize logic for testability and consistency. + +### Interface Sketch + +```go +package resilience + +import ( + "context" + "time" + + "github.com/cenkalti/backoff/v4" + gocache "github.com/patrickmn/go-cache" + "golang.org/x/sync/singleflight" +) + +// Config holds resilience settings. +type Config struct { + EnableRetry bool + MaxRetries int + InitialBackoff time.Duration + MaxBackoff time.Duration + MaxElapsedTime time.Duration + EnableCache bool + CacheTTL time.Duration + EnableSingleflight bool +} + +// RetryableError wraps an error with retry/permanent classification. +type RetryableError interface { + error + IsRetryable() bool + IsPermanent() bool + RetryAfter() time.Duration // 0 if no hint +} + +// WithRetry wraps an operation with exponential backoff + Retry-After honor. +func WithRetry[T any](ctx context.Context, cfg Config, operation func(context.Context) (T, error)) (T, error) { + if !cfg.EnableRetry { + return operation(ctx) + } + + b := backoff.NewExponentialBackOff() + b.InitialInterval = cfg.InitialBackoff + b.MaxInterval = cfg.MaxBackoff + b.MaxElapsedTime = cfg.MaxElapsedTime + bCtx := backoff.WithContext(b, ctx) + + retryOp := func() (T, error) { + result, err := operation(ctx) + if err == nil { + return result, nil + } + + if retryableErr, ok := err.(RetryableError); ok { + if retryableErr.IsPermanent() { + return result, backoff.Permanent(err) + } + if retryableErr.IsRetryable() { + // Honor Retry-After + if delay := retryableErr.RetryAfter(); delay > 0 { + time.Sleep(delay) + } + return result, err // retry + } + } + + // Unknown error — don't retry + var zero T + return zero, backoff.Permanent(err) + } + + return backoff.RetryWithData(retryOp, bCtx) +} + +// WithCache wraps an operation with TTL caching. +func WithCache[T any](cache *gocache.Cache, key string, ttl time.Duration, operation func() (T, error)) (T, error) { + if cache == nil { + return operation() + } + + // Check cache + if cached, found := cache.Get(key); found { + return cached.(T), nil + } + + // Cache miss — execute operation + result, err := operation() + if err != nil { + // On error, check if stale cache exists (graceful degradation) + if cached, found := cache.Get(key); found { + return cached.(T), nil + } + var zero T + return zero, err + } + + // Cache successful result + cache.Set(key, result, ttl) + return result, nil +} + +// WithSingleflight wraps an operation with request coalescing. +func WithSingleflight[T any](group *singleflight.Group, key string, operation func() (T, error)) (T, error) { + result, err, _ := group.Do(key, func() (interface{}, error) { + return operation() + }) + if err != nil { + var zero T + return zero, err + } + return result.(T), nil +} +``` + +### Usage in Metrics Client + +```go +// In /Users/clavery/code/b2c-developer-tooling/feature-grafana/packages/b2c-tooling-sdk-go/clients/metrics/client.go + +import "github.com/salesforce/b2c-tooling-sdk-go/resilience" + +func (c *Client) getMetricsWithResilience(ctx context.Context, category string, window metricsops.ResolvedMetricsWindow, filters map[string]string) (*MetricsDataResponse, error) { + cfg := resilience.Config{ + EnableRetry: true, + InitialBackoff: 1 * time.Second, + MaxBackoff: 30 * time.Second, + MaxElapsedTime: 2 * time.Minute, + } + + return resilience.WithRetry(ctx, cfg, func(ctx context.Context) (*MetricsDataResponse, error) { + return c.getMetrics(ctx, category, window, filters) + }) +} +``` + +### Usage in Datasource Backend (with Cache + Singleflight) + +```go +// In /Users/clavery/code/b2c-developer-tooling/feature-grafana/packages/b2c-grafana-datasource/pkg/plugin/datasource.go + +probe, err := resilience.WithSingleflight(d.singleflightGroup, cacheKey, func() (*metricsops.CategoryProbe, error) { + return resilience.WithCache(d.probeCache, cacheKey, 10*time.Minute, func() (*metricsops.CategoryProbe, error) { + return probeCategory(ctx, category, client) + }) +}) +``` + +**Files changed:** +- New package: `/Users/clavery/code/b2c-developer-tooling/feature-grafana/packages/b2c-tooling-sdk-go/resilience/resilience.go` +- Update: `clients/metrics/client.go`, `clients/cip/client.go`, `pkg/plugin/datasource.go`, `pkg/cip/plugin/datasource.go` + +**Effort:** 6–8 hours (includes tests for retry logic, cache eviction, singleflight deduplication) + +**Benefits:** +- ✅ Both Metrics and CIP clients use same retry logic +- ✅ Testable: mock operation func, verify retry count, backoff delays +- ✅ Consistent error handling (RetryableError interface) +- ✅ Easy to add circuit-breaker later (extend Config) + +--- + +## 8. Sequenced Implementation Checklist + +**Each item is independently shippable + verifiable against live bdpx_prd.** + +### Phase 1: Error Visibility (1–2 days) + +- [ ] 1. **Define `HTTPError` type** in `b2c-tooling-sdk-go/clients/metrics/errors.go` + - Fields: `StatusCode`, `Body`, `RetryAfter` + - Methods: `Error()`, `IsRetryable()`, `IsPermanent()` + - Helper: `ParseRetryAfter(header string) time.Duration` + - **Verify:** Unit test parsing `Retry-After: 30` and `Retry-After: Wed, 21 Oct 2026 07:28:00 GMT` + +- [ ] 2. **Update `getMetrics` to return `HTTPError`** in `clients/metrics/client.go` + - Parse `resp.StatusCode`, extract `Retry-After` header + - Replace `fmt.Errorf(...)` with `&HTTPError{...}` + - **Verify:** Integration test against mock server returning 429 + Retry-After header; assert error type and RetryAfter value + +- [ ] 3. **Add `mapMetricsErrorToDataResponse` helper** in `pkg/plugin/datasource.go` + - Map `HTTPError.StatusCode` → `backend.StatusTooManyRequests`, `StatusBadGateway`, etc. + - Use `backend.ErrorSourceFromHTTPStatus(statusCode)` for source attribution + - Return `backend.ErrDataResponseWithSource(...)` + - **Verify:** Unit test with mock HTTPError{StatusCode: 429}; assert result has Status=429, ErrorSource=downstream + +- [ ] 4. **Update QueryData error handling** (line ~223 in `datasource.go`) + - Replace `backend.ErrDataResponse(StatusInternal, ...)` with `mapMetricsErrorToDataResponse(err)` + - **Verify:** E2E test against mock server returning 429; check Grafana panel shows orange "Rate limited" badge (not red "Error") + +### Phase 2: Retry + Backoff (1–2 days) + +- [ ] 5. **Add `getMetricsWithRetry` wrapper** in `clients/metrics/client.go` + - Configure `backoff.NewExponentialBackOff()`: `InitialInterval=1s`, `MaxInterval=30s`, `MaxElapsedTime=2min` + - Wrap `getMetrics` in `backoff.RetryWithData` + - Honor `HTTPError.RetryAfter` by sleeping before retry + - Mark permanent errors with `backoff.Permanent(err)` + - **Verify:** Integration test with mock server: 429 (1st call) → 200 (2nd call); assert retry occurred and RetryAfter was honored + +- [ ] 6. **Update `Get*Metrics` methods** to call `getMetricsWithRetry` instead of `getMetrics` + - **Verify:** Regression test existing success cases; assert no behavior change + +- [ ] 7. **Load test against bdpx_prd** + - Dashboard with 5 panels, 1min refresh, inject 429 (if possible via rate-limit trigger) + - Observe retry attempts in logs, confirm exponential backoff (1s, 2s, 4s, ...) + - Confirm panels recover after API unblocks + - **Verify:** No "plugin error" in Grafana; panels show "Rate limited" badge during 429, then recover + +### Phase 3: Discovery Caching (Highest ROI) (1 day) + +- [ ] 8. **Add `probeCache` field** to `MetricsDatasource` struct + - Initialize with `gocache.New(10*time.Minute, 15*time.Minute)` + - Add `github.com/patrickmn/go-cache` dependency + - **Verify:** Datasource instantiation succeeds; no panics + +- [ ] 9. **Add `probeCategoryWithCache` wrapper** in `datasource.go` + - Cache key: `fmt.Sprintf("%s:%s:%s", datasourceUID, category, tenantId)` + - Check cache before calling `probeCategory` + - On error, return stale cache if available (graceful degradation) + - **Verify:** Unit test: 1st call caches, 2nd call hits cache (no API call) + +- [ ] 10. **Update CallResource handlers** (`handleGetMetrics`, `handleLabelKeys`, `handleLabelValues`) + - Replace `probeCategory(...)` with `probeCategoryWithCache(...)` + - **Verify:** E2E test: open query editor, observe 1 probe API call; close/reopen editor within 10min, observe 0 probe calls + +- [ ] 11. **Load test discovery probes** + - Rapidly open/close query editor 10x in 1min + - Without cache: expect 10 probe calls + - With cache: expect 1 probe call + - **Verify:** Logs show "probe cache hit" messages; bdpx_prd API call volume drops 90% + +### Phase 4: Health Check Resilience (0.5 days) + +- [ ] 12. **Add `healthCache` field** to `MetricsDatasource` struct + - Initialize with `gocache.New(5*time.Minute, 10*time.Minute)` + - **Verify:** No panics + +- [ ] 13. **Update `CheckHealth` method** with retry + cache fallback + - 3 retry attempts with 1s, 2s, 3s backoff + - Cache successful health result (5min TTL) + - On failure after retries, return cached OK if available + - **Verify:** Integration test: mock server fails 2x, succeeds 3rd; assert health returns OK; cache entry exists + +- [ ] 14. **Test health check with transient 429** + - Mock server: 429 (1st call) → 200 (2nd call) + - Assert: Save & Test returns OK (retried successfully) + - Mock server: 429 (all calls) + stale cache exists + - Assert: Save & Test returns OK (graceful degradation) + - **Verify:** UI shows datasource as healthy despite transient 429 + +### Phase 5: Query Caching + Singleflight (Optional, 2–3 days) + +- [ ] 15. **Add `queryCache` + `singleflightGroup` fields** to `MetricsDatasource` + - Initialize `queryCache` with 2min TTL + - Initialize `singleflight.Group` + - **Verify:** No panics + +- [ ] 16. **Add `buildQueryCacheKey` helper** + - Hash `(datasourceUID, category, roundedFrom, roundedTo, filters, tenantId)` + - Round timestamps to 1min buckets + - **Verify:** Unit test: identical queries → same key; different times within same minute → same key + +- [ ] 17. **Wrap QueryData calls** with singleflight + cache + - Check cache first; return if hit + - Wrap GetXMetrics in `singleflightGroup.Do(...)` + - Cache successful responses + - **Verify:** Unit test: 5 concurrent identical queries → 1 API call (singleflight); 2nd batch within TTL → 0 API calls (cache) + +- [ ] 18. **Load test dashboard refresh** + - 10-panel dashboard, 30s refresh, 24h window + - Measure API call volume: before (10 calls/refresh) vs after (1–2 calls/refresh + cache hits) + - **Verify:** bdpx_prd API call volume drops 80–90%; no rate limit errors + +### Phase 6: CIP Resilience (1–2 days) + +- [ ] 19. **Add `QueryWithRetry` wrapper** in `clients/cip/client.go` + - Configure backoff: `InitialInterval=500ms`, `MaxElapsedTime=30s` + - Detect retryable errors by text matching (`timeout`, `connection`) + - Mark permanent errors (`unauthorized`, `syntax error`) + - **Verify:** Integration test: mock db fails with timeout (1st call) → succeeds (2nd call); assert retry occurred + +- [ ] 20. **Update `Query`, `ListTables`, `DescribeColumns`** to call `QueryWithRetry` + - **Verify:** Regression test; no behavior change on success + +- [ ] 21. **Add `schemaCache` to CIP datasource** + - Cache `ListTables`, `DescribeColumns`, `Sites` (20min TTL) + - Wrap CallResource handlers + - **Verify:** E2E test: query editor /tables call caches; 2nd call hits cache + +- [ ] 22. **Map CIP errors** to `backend.ErrDataResponseWithSource(StatusInternal, ErrorSourceDownstream, ...)` + - All CIP errors → downstream source (can't extract HTTP status) + - **Verify:** E2E test: CIP query fails → Grafana shows "downstream error", not "plugin error" + +### Phase 7: Documentation + Final Verification (1 day) + +- [ ] 23. **Document rate-limit best practices** in README + docs site + - Min refresh interval (5–10min) + - maxDataPoints impact + - 429 error recovery behavior + - **Verify:** Docs build successfully; links work + +- [ ] 24. **End-to-end load test against bdpx_prd** + - 10-panel Metrics dashboard, 1min refresh, 7d window + - 5-panel CIP dashboard, 2min refresh, 1d window + - Run for 30min; monitor API call volume, 429 rate, cache hit rate + - **Verify:** Zero 429 errors after initial burst; API calls stable at < 10/min; cache hit rate > 80% + +- [ ] 25. **Changeset + PR** + - Changeset: `'b2c-grafana-datasource': minor` (new resilience features) + - PR title: `@W- Add rate-limit resilience (429 handling, retry, caching) to Grafana datasource` + - PR summary: Tier 1 + Tier 2 features, before/after metrics, testing notes + - **Verify:** CI passes; review approved + +--- + +## Summary: Why This Plan Works + +1. **Incremental + Verifiable**: Each phase is independently testable against live API; no big-bang merge risk. +2. **Prioritized by Impact**: Tier 1 (error visibility + retry + discovery caching) delivers 80% of value in 3–4 days; Tier 2 (query caching + singleflight) polishes to 95%. +3. **Grounded in SDK Reality**: Every recommendation uses verified `grafana-plugin-sdk-go` v0.257.0 symbols (`backend.StatusTooManyRequests`, `ErrorSourceFromHTTPStatus`, `instancemgmt` lifecycle). +4. **OSS-Friendly**: No Enterprise/Cloud-only features in Tier 1–2; clearly flags Tier 3 (Enterprise query caching) as optional. +5. **CIP-Aware**: Recognizes database/sql layer can't use HTTP middleware; provides separate retry pattern for Avatica path. +6. **Testable Abstraction**: `resilience` package centralizes retry+cache+singleflight for both Metrics and CIP; mock-friendly interface. +7. **Production-Ready**: After Phase 4, plugin survives 429s gracefully (proper status codes, downstream attribution, retry, cached probes). After Phase 5, API load drops 80–90% (cache + coalescing). + +**Bottom line:** Tier 1 makes the plugin *safe* for production (won't crash on 429, proper error attribution). Tier 2 makes it *efficient* (low API footprint, dashboard-friendly). Implement Tier 1 first (1 week), Tier 2 second (1 week), ship to users. \ No newline at end of file diff --git a/packages/b2c-grafana-datasource/REVIEW_HOSTILE.md b/packages/b2c-grafana-datasource/REVIEW_HOSTILE.md new file mode 100644 index 000000000..3a366682e --- /dev/null +++ b/packages/b2c-grafana-datasource/REVIEW_HOSTILE.md @@ -0,0 +1,91 @@ +# Hostile-Agent Review — B2C Grafana Plugin + Go SDK + +_External adversarial review (2026-07-14). Captured verbatim as an independent second +reviewer, to be reconciled with the internal review workflow's synthesized plan +(see the review-polish plan output). Spot-verified claims annotated **[VERIFIED]** / +**[VERIFIED-LIVE]** / **[PARTIAL]** where checked against current code._ + +## Verdict + +Credible exploratory POC; **not** ready for shared deployment or Grafana catalog +packaging. Main problems: functional query failures, unbounded SQL behavior, transport +lifecycle issues, build/CI gaps. + +## Critical + +1. **Default CIP query cannot execute.** `src/cip/types.ts:17` emits + `$__timeGroupAlias(..., $__interval)`, but `pkg/cip/plugin/macros.go:25` only accepts + literal durations — `$__interval` is left in the SQL. Also `5m`/`15m`/`6h` reduce to + MINUTE/HOUR, losing the multiplier → wrong buckets. **[VERIFIED]** default query uses + `$__interval` (unhandled); `calciteFloorUnit` keys on last char only (multiplier dropped). +2. **Metrics queries wider than 24h fail.** SDK declares 24h max (`window.go:16`) but + explicit Grafana ranges pass through unchanged (`pkg/plugin/datasource.go:182`). + "Last 7 days" → API 400. Fix: partition into ≤24h requests, merge by metric/labels, + sort, dedupe boundary points. **[VERIFIED]** window passed through as-is. +3. **CIP template interpolation is not SQL-safe.** `src/cip/datasource.ts:29` does generic + string replace without Calcite quoting/escaping; textbox/URL variables can alter query + syntax. The `variable` resource also executes arbitrary submitted SQL + (`pkg/cip/plugin/datasource.go:227`). Fix: SQL-aware interpolation, SELECT-only + validation where feasible, documented read-only CIP account. +4. **CIP result processing unbounded.** `sqlutil.FrameFromRows(rows, -1, …)` + (`pkg/cip/plugin/datasource.go:124`) = unlimited rows; variable queries materialize all + rows into maps before dedupe. Broad query → plugin OOM. Mature SQL sources enforce a row + limit (Grafana MySQL). **[VERIFIED]** `-1`. + +## High + +5. **OAuth token acquisition can hang.** Both token sources use `context.Background()` and + clients without timeouts (`auth/oauth.go:87`); query cancellation doesn't bound a + stalled token request. Metrics HTTP calls also lack a client timeout. Fix: accept a + caller transport/client or set explicit connect/response-header/total timeouts. +6. **Per-query tenancy → unbounded credential/client cache.** Arbitrary `tenantId` creates + permanent clients + token sources (`pkg/plugin/datasource.go:30`). Fix: allowlist or + normalized same-realm validation + bounded TTL/LRU cache. **[PARTIAL]** cache is + same-realm-gated already; but it's an unbounded `map`, no TTL/LRU — unbounded growth real. +7. **Normal build path doesn't produce the CIP plugin.** `package.json:9`, `Magefile.go:10`, + Makefile build only Metrics. `build:backend:all` exits 0 with no binary when Mage absent + (reproduced). Only the Dockerfile builds CIP. **[VERIFIED]**. +8. **Clean CI install fails.** manifest vs `pnpm-lock.yaml:404` disagree on 17 dep entries; + `pnpm install --frozen-lockfile` → `ERR_PNPM_OUTDATED_LOCKFILE`. **[VERIFIED-LIVE]**. +9. **New Go code effectively outside CI.** `.github/workflows/ci.yml:58` runs workspace + scripts but never configures Go or runs Go tests/vet. Grafana package has no + test:agent/lint:agent/backend build hook. Coverage: 0% Metrics backend & CIP client, + 18.3% CIP backend. +10. **Discovery amplifies rate limits.** Opening the Metrics editor does multiple 24h + probes; every label-value picker fetches a full category response. No cache/singleflight + (`pkg/plugin/datasource.go:631`). (Overlaps RESILIENCE_PLAN.md.) +11. **Two-plugin artifact not structured for signing/release.** Frontend build nests CIP + beneath the Metrics `dist`; Grafana expects one plugin-root archive per plugin ID with + its own executable + manifest (one MANIFEST.txt per root). Split release artifacts even + if source stays one workspace package. + +## Medium + +12. Metrics responses keyed by JSON `qm.RefID` not authoritative `query.RefID` + (`pkg/plugin/datasource.go:176`); missing/inconsistent JSON can lose the response. + **[VERIFIED]**. +13. All downstream 400/401/403/429/5xx → `StatusInternal`, no `ErrorSourceDownstream`, no + retry metadata, no typed SDK errors. (Overlaps RESILIENCE_PLAN.md.) +14. Cross-language catalog duplicated not generated end-to-end: TS generator writes + `packages/b2c-tooling-sdk/scripts/generate-metrics-tags-catalog.ts:23`, Go embeds a copy + at `tags.go:14`. Currently hash-identical but no test enforces it. +15. Grafana compatibility overclaimed: manifests say `>=9.0.0`, frontend compiles against + 10.4, Docker tests only 11.2. Add `@grafana/plugin-e2e` across supported versions + run + Plugin Validator. +16. Go Metrics client only superficially typed: constructors can't report invalid + URL/config, filters are unrestricted maps, errors are strings not inspectable types. + Prefer validated constructors, category-specific options, typed HTTP error, injected + `http.Client`/`RoundTripper`. +17. Untracked mock server includes a ~3 MB arm64 Mach-O binary not ignored + (`demo/mock-metrics/mock-metrics`). **[VERIFIED]** git-tracked; `.gitignore` only has `dist/`. + +## Reference points (mature datasources) +InfluxDB defaults MaxSeries=1000, uses Grafana HTTPClientOptions/TLS/timeout/proxy/PDC. +Grafana recommends bounded concurrent QueryData, sqlutil/sqlds, cached connections, backend +telemetry, resource handlers for editor metadata. + +## Reviewer verification +Passed: Go tests w/ race, vet, module verify, SDK 2,067-test suite, SDK typecheck, plugin +FE typecheck/build, ESLint, compose config, cross-language fixture byte parity. +Failed: frozen-lockfile install. (Root lint/format also fail on unrelated pre-existing MCP +import-resolution + VS-extension CSS issues.) No files changed during review. diff --git a/packages/b2c-grafana-datasource/REVIEW_PLAN.md b/packages/b2c-grafana-datasource/REVIEW_PLAN.md new file mode 100644 index 000000000..33e1915a1 --- /dev/null +++ b/packages/b2c-grafana-datasource/REVIEW_PLAN.md @@ -0,0 +1,157 @@ +# B2C Grafana Plugin + Go SDK — Consolidated Review & Polish Plan + +> **Progress (2026-07-14):** sqlds adoption + Tier 0 + Tier 1 correctness/error-source DONE +> and live-verified vs bdpx_prd. Hygiene/SDK-tests/CI/docs sweep ran in worktrees (reconciled +> into main). See the "IMPLEMENTED" markers below. +> +> - ✅ **CIP → grafana/sqlds/v4** — driver.go (Connect/Settings/Macros/Converters), Calcite +> macros via sqlutil (fixes `$__interval` + multiplier bugs structurally), RowLimit+Timeout +> from the framework, format is now a numeric enum (0=time_series/1=table), macros.go deleted, +> ResponseMutator dropped (sqlds does long→wide). Live: time-series, dimension-pivot, table all pass. +> - ✅ **Tier 0** — lockfile regenerated (frozen install passes); default CIP query fixed via sqlds; +> Metrics >24h range partitioning (clients/metrics/partition.go, merge/sort/dedupe) — "Last 7 days" +> now returns 2801 pts over 168h (was 400). +> - ✅ **Tier 1** — RefID keyed by authoritative query.RefID; typed `HTTPError` (errors.go) + bounded +> error-body read; `mapMetricsError` → StatusTooManyRequests/BadGateway + ErrorSourceDownstream; +> idempotent timestamp normalization; discovery probe errors surfaced (not empty dropdowns); +> frontend valueCache cleared on category change. +> - ✅ **Sweep — DONE + reconciled into main** (carefully, not blind-copied: the docs worktree had +> branched from a pre-sqlds snapshot, so only the genuinely-new doc artifacts were pulled; all +> source/build files kept from the current tree). Landed: mock binary untracked + gitignored; +> transient docs deleted (BOOT-VERIFICATION-REPORT/DEMO.boot.log/STACK-VERIFICATION); Go copyright +> headers; CIP client_test.go + metadata_test.go; build scripts build BOTH binaries (no mage +> fake-success); catalog parity test (TS↔Go drift tripwire, in CI); `.github/workflows/ci.yml` +> test-go job (Go 1.26, both modules build/vet/test); docs consolidated → `docs/{quickstart, +> configuration,query-editor,api-reference,architecture}.md` (old QUICKSTART/DEMO/INTEGRATION/ +> BACKEND-CONTRACT/ARCHITECTURE removed, README links in); expanded SDK README; CLAUDE.md lists +> both new packages + a Grafana/Go-SDK dev section. +> +> **Final main-tree state: all Go tests pass (SDK incl. cip + parity, plugin), both binaries build, +> frozen lockfile clean, frontend typechecks. sqlds CIP + Tier-0/1 live-verified vs bdpx_prd.** +> - ⬜ **Deferred** — RESILIENCE_PLAN Tier 1 (retry/backoff/cache/singleflight); plugin.json +> screenshots+signing artifact split; frontend loading states/memo; OAuth timeout; Metrics +> VariableQueryEditor; wholeAs panic guard + DECIMAL-parse warn (DECIMAL warn already added in sqlds driver). + + +_Reconciles two independent reviews (2026-07-14): an internal 30-agent workflow +(73 findings → 19 adversarially verified) and an external hostile-agent review +(`REVIEW_HOSTILE.md`). Rate-limit/caching items cross-reference `RESILIENCE_PLAN.md`._ + +## Overall verdict + +**Credible, end-to-end-working POC — not yet ready for shared deployment or catalog +signing.** Both reviews independently agree. The architecture (dual datasource, Go SDK, +macros, discovery, dashboards) is sound; the gaps are functional bugs, missing bounds, +error attribution, and build/CI/release hygiene. + +**Confidence signal:** where both reviews overlap (functional query bugs, error-source, +discovery amplification, build/CI, docs noise) treat as high-confidence. Disagreements +were resolved by direct code check (noted inline). + +--- + +## Tier 0 — Ship-blockers (broken on first contact) — DO FIRST + +| # | Issue | File | Source | Verified | +|---|---|---|---|---| +| 0.1 | **`pnpm install --frozen-lockfile` FAILS** — lockfile 17 specifiers stale after the @grafana/toolkit→webpack migration. Breaks clean CI for the whole repo. | `pnpm-lock.yaml` vs `package.json` | hostile #8 | ✅ live | +| 0.2 | **Default CIP query cannot execute** — `types.ts:19` emits `$__timeGroupAlias(submit_date, $__interval)`; `macros.go` doesn't handle `$__interval` (only literal durations) → left in SQL. Also `calciteFloorUnit` keys on last char → `5m`/`15m`/`6h` lose the multiplier (→ MINUTE/HOUR, wrong buckets). | `src/cip/types.ts:19`, `pkg/cip/plugin/macros.go` | hostile #1 | ✅ code | +| 0.3 | **Metrics >24h ranges 400** — SDK declares 24h max but `datasource.go:182` passes Grafana range through unchanged. "Last 7 days" fails. Needs request partitioning (split ≤24h, merge by metric/labels, sort, dedupe boundaries). | `pkg/plugin/datasource.go:182` | hostile #2 | ✅ code | + +Fix 0.1 + 0.2 are trivial and make the plugin stop looking broken. 0.3 is a small backend loop. + +--- + +## Tier 1 — Correctness + +**From hostile review:** +- **1.1 Unbounded CIP rows** — `FrameFromRows(rows, -1, …)` (`pkg/cip/plugin/datasource.go:124`) + variable queries materialize all rows into maps → OOM risk. Enforce a row cap (mature SQL sources do; e.g. MaxSeries=1000 in InfluxDB). ✅ verified. +- **1.2 RefID keying** — success/error paths key `response.Responses[qm.RefID]` (from JSON) not authoritative `query.RefID`. Missing `refId` in JSON loses the response. `pkg/plugin/datasource.go:176`. ✅ verified. +- **1.3 OAuth token acquisition can hang** — token sources use `context.Background()` + no client timeout (`auth/oauth.go:87`); query cancel doesn't bound a stalled token/HTTP call. Add explicit timeouts / accept injected client. (hostile #5) +- **1.4 CIP template interpolation not SQL-safe** — `src/cip/datasource.ts:29` generic string replace, no Calcite quoting. **NOTE:** internal review's adversarial pass ruled *arbitrary-SQL-in-editor* is **by design** (Grafana SQL datasources intentionally let editors run SQL). So the real, narrower issue = **variable/URL-controlled values** altering query syntax + the need for a **documented read-only CIP account**. Scope to: SQL-safe variable interpolation + SELECT-only validation where feasible + docs. (hostile #3, refined) + +**From internal review (adversarially verified survivors):** +- **1.5 Label-values cache stale on category change** — `valueCache` keyed by label key only, not category; `onCategoryChange` doesn't clear it → wrong values after category switch. `src/QueryEditor.tsx` (`setValueCache({})` in `onCategoryChange`). ✅ HIGH, 5 min. +- **1.6 `strategyWholeAs` panics on empty key** — violates `ParseSeriesTags` "never panics" contract; only reachable via hand-edited catalog. Add init-time catalog validation + graceful degradation. `operations/metrics/tags.go:201`. MEDIUM. +- **1.7 Discovery failures return empty arrays, hide errors** — `probeCategory` nil→empty dropdowns; user can't tell "no data" from "API down". Return `sendErr` like CIP does. `pkg/plugin/datasource.go:666+`. MEDIUM. +- **1.8 CIP DECIMAL parse errors silently null'd** — add a Warn log (revenue/AOV/latency columns; silent nulls hide data-quality issues). `pkg/cip/plugin/datasource.go:166`. 5 min. +- **1.9 Timestamp normalization mutates in place** — not a bug yet, but a landmine for the planned response cache (double ×1000). Make idempotent or document. `clients/metrics/client.go:156`. (also = RESILIENCE_PLAN interaction) + +--- + +## Tier 2 — Grafana idiom (measured vs sqlds / postgres / Infinity) + +- **2.1 ⭐ Adopt `github.com/grafana/sqlds` for CIP?** Both reviews flag hand-rolled `QueryData`. Internal review **recommends adopting** (standard for Postgres/MySQL/ClickHouse; gives retry/timeout/tracing/maxDataPoints; our `cip.Client.DB()` + converters make it feasible; conditional wide-framing moves to a `ResponseMutator`). **Decision needed** — adopt before release, or document why hand-rolled is justified. ~3–4h prototype. +- **2.2 Error-source attribution** — all downstream 400/401/403/429/5xx → `StatusInternal`, no `ErrorSourceDownstream`, no typed errors. Both reviews + already designed in `RESILIENCE_PLAN.md §3`. 429 should show as downstream/rate-limited, not a plugin bug. +- **2.3 Rate-limit resilience** (hostile #10, #13; RESILIENCE_PLAN) — discovery/probe amplification, no retry/backoff/Retry-After, no cache/singleflight. Governed by `RESILIENCE_PLAN.md` (separate track). +- **2.4 `plugin.json` completeness** — dynamic version (not hardcoded 0.1.0), screenshots, build metadata; needed for signing. Both reviews. `src/plugin.json`, `src/cip/plugin.json`. +- **2.5 Release/signing artifact structure** (hostile #11) — frontend nests CIP under Metrics `dist`; Grafana signing wants one plugin-root archive per plugin ID (own executable + MANIFEST.txt). Split release artifacts (source can stay one workspace pkg). +- **2.6 Compatibility overclaim** (hostile #15) — manifests say `>=9.0.0`, FE compiles vs 10.4, Docker tests only 11.2. Add `@grafana/plugin-e2e` + Plugin Validator; set an honest floor. +- **2.7 Lower-impact idiom** — `PreferredVisualisationType` hint on frames; lightweight `CheckHealth` (Metrics fetches 5min of data vs CIP's `Ping`); add a Metrics `VariableQueryEditor` (CIP has one; `metricFindQuery` already supports it). + +--- + +## Tier 3 — Frontend polish +- **3.1** Loading state (`isLoading`) on Metrics discovery dropdowns (CIP shows "Loading tables…"; Metrics shows nothing). `src/QueryEditor.tsx`. 15 min. +- **3.2** `useMemo` `labelKeyOptions` + hoist `CATEGORY_OPTIONS` out of component. 5 min. +- **3.3** (optional) `@testing-library/react` tests for the editors (~650 LOC untested stateful logic). + +## Tier 4 — SDK as publishable artifact +- **4.1** Copyright headers on **all `.go` files** (TS has them; Go doesn't). _Resolves internal review's own §7.1 miss._ +- **4.2** CIP client has **no tests** (`clients/cip` — no `*_test.go`); metrics/tenant/window/tags/oauth are tested. Add sticky-session + query + metadata tests. +- **4.3** Typed inspectable errors (`HTTPError` w/ `Is`/`As`) — shared with RESILIENCE_PLAN §3.1. (hostile #16) +- **4.4** Validated constructors (report bad URL/config), category-specific options instead of unrestricted `map[string]string` filters, injectable `http.Client`. (hostile #16) +- **4.5** Module-path decision — keep monorepo path (works) vs separate repo for clean `go get`. Document choice in SDK README. +- **4.6** Confirm **zero Grafana-SDK leakage** in the SDK module (it must not import grafana-plugin-sdk-go) — internal review confirms clean; keep a guard. + +## Tier 5 — Docs (see §Documentation plan below) +## Tier 6 — Repo hygiene +- **6.1 Committed 3MB `mock-metrics` arm64 binary** is git-tracked; `.gitignore` only has `dist/`. `git rm` + ignore. _Hostile #17 correct; internal §7.1 wrong — resolved by direct check._ +- **6.2 Go code outside CI** (hostile #9) — `.github/workflows/ci.yml` never configures Go or runs `go test`/`vet`; plugin pkg has no `test:agent`/`lint:agent`. 0% coverage on Metrics backend + CIP client. Wire both Go modules + the plugin into CI. +- **6.3 Normal build omits CIP** (hostile #7) — `build:backend`/Makefile/Magefile build only `gpx_b2c_metrics`; CIP is Docker-only. `build:backend:all` fake-succeeds when mage absent. Fix build scripts to produce both binaries. +- **6.4 Cross-language catalog parity untested** (hostile #14) — TS `generate-metrics-tags-catalog.ts` and Go `tags.go` embed copies; identical now but nothing enforces it. Add a CI parity check (regenerate + diff, or hash-compare). +- **6.5 Changeset/versioning** — new packages aren't in the changeset allow-list; Go module is git-tag versioned. Decide: git tags for Go SDK + plugin, changeset only for docs. Add to `.changeset/config.json ignore` + `pnpm-workspace.yaml` as needed. +- **6.6 Makefile/compose robustness** — `docker compose down` fails on env interpolation in real mode (observed); guard the down target. + +--- + +## Documentation plan + +**DELETE (transient dev noise, git rm):** `BOOT-VERIFICATION-REPORT.md`, `DEMO.boot.log`, +`STACK-VERIFICATION.md`, the tracked `demo/mock-metrics/mock-metrics` binary. Consider +`REVIEW_HOSTILE.md`/`REVIEW_PLAN.md`/`RESILIENCE_PLAN.md` as contributor docs (keep, or move +under a `docs/dev/`). + +**CONSOLIDATE (README/QUICKSTART/DEMO/INTEGRATION/ARCHITECTURE/BACKEND-CONTRACT overlap → target):** +``` +packages/b2c-grafana-datasource/ +├── README.md — overview, features, quick install, links +├── docs/ +│ ├── quickstart.md — merge QUICKSTART + DEMO (one Docker walkthrough) +│ ├── configuration.md — datasource settings, OAuth, demo vs real, multi-tenant +│ ├── query-editor.md — Metrics tiered filters + CIP SQL/macros/schema/variables +│ ├── api-reference.md — CallResource endpoints (was INTEGRATION.md) +│ └── architecture.md — ARCHITECTURE.md + BACKEND-CONTRACT.md merged +└── (dev) RESILIENCE_PLAN.md, REVIEW_PLAN.md +``` + +**WRITE (gaps):** +- Plugin **user guide** (install signed/unsigned, add both datasources, first dashboard, query editor, template variables, alerting, troubleshooting, **read-only CIP account** guidance). +- Go SDK **usage guide** — expand `b2c-tooling-sdk-go/README.md`: install, quick start, package overview, error handling, testing; link to godoc. (godoc package comments already present ✅.) +- **Monorepo integration**: add Grafana plugin + Go SDK entries to `docs/` Vitepress site + sidebar; update root **CLAUDE.md** (new packages + build commands). + +--- + +## Recommended execution order + +1. **Tier 0 ship-blockers** (0.1 lockfile, 0.2 default query + macro multiplier, 0.3 >24h partitioning) — hours. *Re-verify vs bdpx_prd.* +2. **Tier 1 correctness** (1.1 row cap, 1.2 RefID, 1.5 cache, 1.6–1.8, 1.3 timeouts, 1.4 var-safety) — ~1 day. +3. **Tier 6 hygiene** (6.1 binary, 6.3 build both, 6.2 CI wiring, 6.4 parity test) — ~0.5 day; unblocks trustworthy CI. +4. **Tier 2 idiom** — error-source (2.2, w/ RESILIENCE_PLAN §3) + **sqlds decision (2.1)** + plugin.json/signing (2.4/2.5) — 1–2 days. +5. **Tier 4 SDK** (headers, CIP tests, typed errors) — ~0.5–1 day. +6. **Tier 5 docs** — ~1 day. +7. **Tier 3 frontend polish** + **RESILIENCE_PLAN Tier 1** — as scheduled. + +**False positives filtered (considered, not real):** discovery-loader memoization (perf non-issue), +webpack-vs-scaffold (intentional dual-entry), CodeEditor onBlur (does pass value), SQL-injection-via-macros +(Grafana editors are trusted-SQL by design — narrowed to variable-value safety, see 1.4). diff --git a/packages/b2c-grafana-datasource/cip b/packages/b2c-grafana-datasource/cip new file mode 100755 index 000000000..2bd337d83 Binary files /dev/null and b/packages/b2c-grafana-datasource/cip differ diff --git a/packages/b2c-grafana-datasource/dashboards/b2c-cip-demo.json b/packages/b2c-grafana-datasource/dashboards/b2c-cip-demo.json new file mode 100644 index 000000000..e3c5132a5 --- /dev/null +++ b/packages/b2c-grafana-datasource/dashboards/b2c-cip-demo.json @@ -0,0 +1,115 @@ +{ + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "datasource": { + "type": "salesforce-b2c-cip-datasource", + "uid": "b2c-cip" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "drawStyle": "line", + "fillOpacity": 10, + "lineWidth": 1, + "showPoints": "never" + }, + "mappings": [], + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "refId": "A", + "format": 0, + "rawSql": "SELECT $__timeGroupAlias(submit_date, '1d'), SUM(num_orders) AS orders\nFROM warehouse.ccdw_aggr_sales_summary\nWHERE $__timeFilter(submit_date)\nGROUP BY 1\nORDER BY 1" + } + ], + "title": "Daily Orders (CIP sales warehouse)", + "type": "timeseries" + }, + { + "datasource": { + "type": "salesforce-b2c-cip-datasource", + "uid": "b2c-cip" + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto" + }, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 9 + }, + "id": 2, + "options": { + "showHeader": true + }, + "targets": [ + { + "refId": "A", + "format": 1, + "rawSql": "SELECT tableSchem, tableName, tableType FROM metadata.TABLES WHERE tableSchem = 'warehouse' ORDER BY tableName" + } + ], + "title": "CIP Warehouse Tables", + "type": "table" + } + ], + "refresh": "", + "schemaVersion": 39, + "tags": [ + "b2c", + "cip", + "analytics" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-3y", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "B2C Commerce Intelligence \u2014 CIP Demo", + "uid": "b2c-cip-demo", + "version": 1, + "weekStart": "" +} \ No newline at end of file diff --git a/packages/b2c-grafana-datasource/dashboards/b2c-cip-merchant.json b/packages/b2c-grafana-datasource/dashboards/b2c-cip-merchant.json new file mode 100644 index 000000000..a213e4b5e --- /dev/null +++ b/packages/b2c-grafana-datasource/dashboards/b2c-cip-merchant.json @@ -0,0 +1,608 @@ +{ + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "id": null, + "links": [], + "panels": [ + { + "id": 1, + "title": "Revenue", + "type": "stat", + "datasource": { + "type": "salesforce-b2c-cip-datasource", + "uid": "b2c-cip" + }, + "description": "Standardized revenue over the selected range (sales-analytics)", + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 0 + }, + "targets": [ + { + "refId": "A", + "rawSql": "SELECT SUM(ss.std_revenue) FROM warehouse.ccdw_aggr_sales_summary ss JOIN warehouse.ccdw_dim_site s ON s.site_id=ss.site_id WHERE $__timeFilter(ss.submit_date) AND s.nsite_id='$site'", + "format": 1 + } + ], + "fieldConfig": { + "defaults": { + "unit": "currencyUSD", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "mappings": [] + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "orientation": "auto", + "textMode": "auto", + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto" + } + }, + { + "id": 2, + "title": "Orders", + "type": "stat", + "datasource": { + "type": "salesforce-b2c-cip-datasource", + "uid": "b2c-cip" + }, + "description": "", + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 0 + }, + "targets": [ + { + "refId": "A", + "rawSql": "SELECT SUM(ss.num_orders) FROM warehouse.ccdw_aggr_sales_summary ss JOIN warehouse.ccdw_dim_site s ON s.site_id=ss.site_id WHERE $__timeFilter(ss.submit_date) AND s.nsite_id='$site'", + "format": 1 + } + ], + "fieldConfig": { + "defaults": { + "unit": "short", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "mappings": [] + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "orientation": "auto", + "textMode": "auto", + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto" + } + }, + { + "id": 3, + "title": "Avg Order Value", + "type": "stat", + "datasource": { + "type": "salesforce-b2c-cip-datasource", + "uid": "b2c-cip" + }, + "description": "", + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 0 + }, + "targets": [ + { + "refId": "A", + "rawSql": "SELECT CAST(SUM(ss.std_revenue)/NULLIF(SUM(ss.num_orders),0) AS DECIMAL(15,2)) FROM warehouse.ccdw_aggr_sales_summary ss JOIN warehouse.ccdw_dim_site s ON s.site_id=ss.site_id WHERE $__timeFilter(ss.submit_date) AND s.nsite_id='$site'", + "format": 1 + } + ], + "fieldConfig": { + "defaults": { + "unit": "currencyUSD", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "mappings": [] + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "orientation": "auto", + "textMode": "auto", + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto" + } + }, + { + "id": 4, + "title": "Units", + "type": "stat", + "datasource": { + "type": "salesforce-b2c-cip-datasource", + "uid": "b2c-cip" + }, + "description": "", + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 0 + }, + "targets": [ + { + "refId": "A", + "rawSql": "SELECT SUM(ss.num_units) FROM warehouse.ccdw_aggr_sales_summary ss JOIN warehouse.ccdw_dim_site s ON s.site_id=ss.site_id WHERE $__timeFilter(ss.submit_date) AND s.nsite_id='$site'", + "format": 1 + } + ], + "fieldConfig": { + "defaults": { + "unit": "short", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "mappings": [] + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "orientation": "auto", + "textMode": "auto", + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto" + } + }, + { + "id": 5, + "title": "Daily Revenue & Orders", + "type": "timeseries", + "datasource": { + "type": "salesforce-b2c-cip-datasource", + "uid": "b2c-cip" + }, + "description": "sales-analytics: daily std revenue (left) and order count (right axis)", + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 4 + }, + "targets": [ + { + "refId": "A", + "rawSql": "SELECT $__timeGroupAlias(ss.submit_date, '1d'), SUM(ss.std_revenue) AS revenue, SUM(ss.num_orders) AS orders FROM warehouse.ccdw_aggr_sales_summary ss JOIN warehouse.ccdw_dim_site s ON s.site_id=ss.site_id WHERE $__timeFilter(ss.submit_date) AND s.nsite_id='$site' GROUP BY 1 ORDER BY 1", + "format": 0 + } + ], + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "drawStyle": "line", + "fillOpacity": 10, + "lineWidth": 1, + "showPoints": "never", + "spanNulls": true, + "axisPlacement": "auto" + }, + "mappings": [], + "unit": "currencyUSD" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "orders" + }, + "properties": [ + { + "id": "unit", + "value": "short" + }, + { + "id": "custom.axisPlacement", + "value": "right" + } + ] + } + ] + }, + "options": { + "legend": { + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "calcs": [ + "mean", + "max", + "lastNotNull" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 6, + "title": "Revenue by Channel / Device", + "type": "table", + "datasource": { + "type": "salesforce-b2c-cip-datasource", + "uid": "b2c-cip" + }, + "description": "revenue-by-channel", + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 12 + }, + "targets": [ + { + "refId": "A", + "rawSql": "SELECT bc.order_channel, ss.device_class_code AS device, SUM(ss.std_revenue) AS revenue, SUM(ss.num_orders) AS orders, CAST(SUM(ss.std_revenue)/NULLIF(SUM(ss.num_orders),0) AS DECIMAL(15,2)) AS aov FROM warehouse.ccdw_aggr_sales_summary ss JOIN warehouse.ccdw_dim_site s ON s.site_id=ss.site_id JOIN warehouse.ccdw_dim_business_channel bc ON bc.business_channel_id=ss.business_channel_id WHERE $__timeFilter(ss.submit_date) AND s.nsite_id='$site' GROUP BY bc.order_channel, ss.device_class_code ORDER BY revenue DESC", + "format": 1 + } + ], + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto" + }, + "mappings": [] + }, + "overrides": [] + }, + "options": { + "showHeader": true, + "cellHeight": "sm", + "footer": { + "show": false + } + } + }, + { + "id": 7, + "title": "Payment Methods", + "type": "table", + "datasource": { + "type": "salesforce-b2c-cip-datasource", + "uid": "b2c-cip" + }, + "description": "payment-method-performance", + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 12 + }, + "targets": [ + { + "refId": "A", + "rawSql": "SELECT pm.display_name AS payment_method, SUM(pss.num_payments) AS payments, SUM(pss.num_orders) AS orders, SUM(pss.std_captured_amount) AS captured FROM warehouse.ccdw_aggr_payment_sales_summary pss JOIN warehouse.ccdw_dim_payment_method pm ON pm.payment_method_id=pss.payment_method_id JOIN warehouse.ccdw_dim_site s ON s.site_id=pss.site_id WHERE $__timeFilter(pss.submit_date) AND s.nsite_id='$site' GROUP BY pm.display_name ORDER BY captured DESC", + "format": 1 + } + ], + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto" + }, + "mappings": [] + }, + "overrides": [] + }, + "options": { + "showHeader": true, + "cellHeight": "sm", + "footer": { + "show": false + } + } + }, + { + "id": 8, + "title": "Top Selling Products", + "type": "table", + "datasource": { + "type": "salesforce-b2c-cip-datasource", + "uid": "b2c-cip" + }, + "description": "top-selling-products", + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 21 + }, + "targets": [ + { + "refId": "A", + "rawSql": "SELECT p.product_display_name AS product, SUM(pss.num_units) AS units, SUM(pss.std_revenue) AS revenue, SUM(pss.num_orders) AS orders FROM warehouse.ccdw_aggr_product_sales_summary pss JOIN warehouse.ccdw_dim_product p ON p.product_id=pss.product_id JOIN warehouse.ccdw_dim_site s ON s.site_id=pss.site_id WHERE $__timeFilter(pss.submit_date) AND s.nsite_id='$site' GROUP BY p.product_display_name ORDER BY revenue DESC LIMIT 25", + "format": 1 + } + ], + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto" + }, + "mappings": [] + }, + "overrides": [] + }, + "options": { + "showHeader": true, + "cellHeight": "sm", + "footer": { + "show": false + } + } + }, + { + "id": 9, + "title": "Top Converting Searches", + "type": "table", + "datasource": { + "type": "salesforce-b2c-cip-datasource", + "uid": "b2c-cip" + }, + "description": "search-query-performance (has_results=true)", + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 21 + }, + "targets": [ + { + "refId": "A", + "rawSql": "SELECT LOWER(sc.query) AS query, SUM(sc.num_searches) AS searches, SUM(sc.num_orders) AS orders, SUM(sc.std_revenue) AS revenue FROM warehouse.ccdw_aggr_search_conversion sc JOIN warehouse.ccdw_dim_site s ON s.site_id=sc.site_id WHERE $__timeFilter(sc.search_date) AND s.nsite_id='$site' AND sc.has_results=TRUE GROUP BY LOWER(sc.query) ORDER BY revenue DESC LIMIT 25", + "format": 1 + } + ], + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto" + }, + "mappings": [] + }, + "overrides": [] + }, + "options": { + "showHeader": true, + "cellHeight": "sm", + "footer": { + "show": false + } + } + }, + { + "id": 10, + "title": "New Registrations by Device", + "type": "timeseries", + "datasource": { + "type": "salesforce-b2c-cip-datasource", + "uid": "b2c-cip" + }, + "description": "customer-registration-trends", + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 30 + }, + "targets": [ + { + "refId": "A", + "rawSql": "SELECT $__timeGroupAlias(r.registration_date, '1d'), r.device_class_code AS device, SUM(r.num_registrations) AS registrations FROM warehouse.ccdw_aggr_registration r JOIN warehouse.ccdw_dim_site s ON s.site_id=r.site_id WHERE $__timeFilter(r.registration_date) AND s.nsite_id='$site' GROUP BY 1, r.device_class_code ORDER BY 1", + "format": 0 + } + ], + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "drawStyle": "line", + "fillOpacity": 10, + "lineWidth": 1, + "showPoints": "never", + "spanNulls": true, + "axisPlacement": "auto" + }, + "mappings": [], + "unit": "short" + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "calcs": [ + "mean", + "max", + "lastNotNull" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 11, + "title": "Top Traffic Referrers", + "type": "table", + "datasource": { + "type": "salesforce-b2c-cip-datasource", + "uid": "b2c-cip" + }, + "description": "top-referrers", + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 30 + }, + "targets": [ + { + "refId": "A", + "rawSql": "SELECT vr.referrer_medium AS medium, vr.referrer_source AS source, SUM(vr.num_visits) AS visits FROM warehouse.ccdw_aggr_visit_referrer vr JOIN warehouse.ccdw_dim_site s ON s.site_id=vr.site_id WHERE $__timeFilter(vr.visit_date) AND s.nsite_id='$site' GROUP BY vr.referrer_medium, vr.referrer_source ORDER BY visits DESC LIMIT 20", + "format": 1 + } + ], + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto" + }, + "mappings": [] + }, + "overrides": [] + }, + "options": { + "showHeader": true, + "cellHeight": "sm", + "footer": { + "show": false + } + } + } + ], + "refresh": "", + "schemaVersion": 39, + "tags": [ + "b2c", + "cip", + "merchant" + ], + "templating": { + "list": [ + { + "name": "site", + "label": "Site", + "type": "query", + "datasource": { + "type": "salesforce-b2c-cip-datasource", + "uid": "b2c-cip" + }, + "query": { + "query": "sites" + }, + "definition": "sites", + "refresh": 1, + "sort": 1, + "includeAll": false, + "multi": false, + "current": { + "text": "Sites-NTOSFRA-Site", + "value": "Sites-NTOSFRA-Site", + "selected": true + }, + "options": [], + "description": "Storefront nsite_id, populated from CIP (distinct ccdw_dim_site.nsite_id).", + "hide": 0 + } + ] + }, + "time": { + "from": "now-90d", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "B2C Commerce \u2014 Merchant Analytics (CIP)", + "uid": "b2c-cip-merchant", + "version": 1, + "weekStart": "" +} \ No newline at end of file diff --git a/packages/b2c-grafana-datasource/dashboards/b2c-cip-technical.json b/packages/b2c-grafana-datasource/dashboards/b2c-cip-technical.json new file mode 100644 index 000000000..9d216b376 --- /dev/null +++ b/packages/b2c-grafana-datasource/dashboards/b2c-cip-technical.json @@ -0,0 +1,442 @@ +{ + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "id": null, + "links": [], + "panels": [ + { + "id": 1, + "title": "Controller Requests / day", + "type": "timeseries", + "datasource": { + "type": "salesforce-b2c-cip-datasource", + "uid": "b2c-cip" + }, + "description": "controller request volume", + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "targets": [ + { + "refId": "A", + "rawSql": "SELECT $__timeGroupAlias(cr.request_date, '1d'), SUM(cr.num_requests) AS requests FROM warehouse.ccdw_aggr_controller_request cr JOIN warehouse.ccdw_dim_site s ON s.site_id=cr.site_id WHERE $__timeFilter(cr.request_date) AND s.nsite_id='$site' GROUP BY 1 ORDER BY 1", + "format": 0 + } + ], + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "drawStyle": "line", + "fillOpacity": 10, + "lineWidth": 1, + "showPoints": "never", + "spanNulls": true, + "axisPlacement": "auto" + }, + "mappings": [], + "unit": "short" + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "calcs": [ + "mean", + "max", + "lastNotNull" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 2, + "title": "Controller Avg Latency (ms)", + "type": "timeseries", + "datasource": { + "type": "salesforce-b2c-cip-datasource", + "uid": "b2c-cip" + }, + "description": "controller avg response time", + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "targets": [ + { + "refId": "A", + "rawSql": "SELECT $__timeGroupAlias(cr.request_date, '1d'), CAST(SUM(cr.response_time)/NULLIF(SUM(cr.num_requests),0) AS DECIMAL(15,2)) AS avg_ms FROM warehouse.ccdw_aggr_controller_request cr JOIN warehouse.ccdw_dim_site s ON s.site_id=cr.site_id WHERE $__timeFilter(cr.request_date) AND s.nsite_id='$site' GROUP BY 1 ORDER BY 1", + "format": 0 + } + ], + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "drawStyle": "line", + "fillOpacity": 10, + "lineWidth": 1, + "showPoints": "never", + "spanNulls": true, + "axisPlacement": "auto" + }, + "mappings": [], + "unit": "ms" + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "calcs": [ + "mean", + "max", + "lastNotNull" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 3, + "title": "Controller Error Rate %", + "type": "timeseries", + "datasource": { + "type": "salesforce-b2c-cip-datasource", + "uid": "b2c-cip" + }, + "description": "controller-error-rate-trend", + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 8 + }, + "targets": [ + { + "refId": "A", + "rawSql": "SELECT $__timeGroupAlias(cr.request_date, '1d'), CAST(100.0*SUM(CASE WHEN cr.status_code BETWEEN 400 AND 599 THEN cr.num_requests ELSE 0 END)/NULLIF(SUM(cr.num_requests),0) AS DECIMAL(15,2)) AS error_pct FROM warehouse.ccdw_aggr_controller_request cr JOIN warehouse.ccdw_dim_site s ON s.site_id=cr.site_id WHERE $__timeFilter(cr.request_date) AND s.nsite_id='$site' GROUP BY 1 ORDER BY 1", + "format": 0 + } + ], + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "drawStyle": "line", + "fillOpacity": 10, + "lineWidth": 1, + "showPoints": "never", + "spanNulls": true, + "axisPlacement": "auto" + }, + "mappings": [], + "unit": "percent" + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "calcs": [ + "mean", + "max", + "lastNotNull" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 4, + "title": "Controller Health Scorecard", + "type": "table", + "datasource": { + "type": "salesforce-b2c-cip-datasource", + "uid": "b2c-cip" + }, + "description": "controller-health-scorecard", + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 16 + }, + "targets": [ + { + "refId": "A", + "rawSql": "SELECT cr.controller_name, SUM(cr.num_requests) AS requests, CAST(SUM(cr.response_time)/NULLIF(SUM(cr.num_requests),0) AS DECIMAL(15,2)) AS avg_ms, CAST(100.0*SUM(CASE WHEN cr.status_code>=400 THEN cr.num_requests ELSE 0 END)/NULLIF(SUM(cr.num_requests),0) AS DECIMAL(15,2)) AS error_pct, CAST(100.0*SUM(cr.num_requests_bucket9+cr.num_requests_bucket10+cr.num_requests_bucket11)/NULLIF(SUM(cr.num_requests),0) AS DECIMAL(15,2)) AS slow_tail_pct, CAST(100.0*SUM(CASE WHEN cr.cache_behavior='HIT' THEN cr.num_requests ELSE 0 END)/NULLIF(SUM(CASE WHEN cr.cache_behavior IN ('HIT','MISS') THEN cr.num_requests ELSE 0 END),0) AS DECIMAL(15,2)) AS cache_hit_pct FROM warehouse.ccdw_aggr_controller_request cr JOIN warehouse.ccdw_dim_site s ON s.site_id=cr.site_id WHERE $__timeFilter(cr.request_date) AND s.nsite_id='$site' GROUP BY cr.controller_name ORDER BY requests DESC LIMIT 25", + "format": 1 + } + ], + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto" + }, + "mappings": [] + }, + "overrides": [] + }, + "options": { + "showHeader": true, + "cellHeight": "sm", + "footer": { + "show": false + } + } + }, + { + "id": 5, + "title": "OCAPI Requests / day", + "type": "timeseries", + "datasource": { + "type": "salesforce-b2c-cip-datasource", + "uid": "b2c-cip" + }, + "description": "OCAPI request volume (all sites)", + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 25 + }, + "targets": [ + { + "refId": "A", + "rawSql": "SELECT $__timeGroupAlias(o.request_date, '1d'), SUM(o.num_requests) AS requests FROM warehouse.ccdw_aggr_ocapi_request o WHERE $__timeFilter(o.request_date) GROUP BY 1 ORDER BY 1", + "format": 0 + } + ], + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "drawStyle": "line", + "fillOpacity": 10, + "lineWidth": 1, + "showPoints": "never", + "spanNulls": true, + "axisPlacement": "auto" + }, + "mappings": [], + "unit": "short" + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "calcs": [ + "mean", + "max", + "lastNotNull" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 6, + "title": "OCAPI Client Usage", + "type": "table", + "datasource": { + "type": "salesforce-b2c-cip-datasource", + "uid": "b2c-cip" + }, + "description": "ocapi-client-usage", + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 25 + }, + "targets": [ + { + "refId": "A", + "rawSql": "SELECT o.client_id, SUM(o.num_requests) AS requests, CAST(100.0*SUM(CASE WHEN o.status_code>=400 THEN o.num_requests ELSE 0 END)/NULLIF(SUM(o.num_requests),0) AS DECIMAL(15,2)) AS error_pct, CAST(SUM(o.response_time)/NULLIF(SUM(o.num_requests),0) AS DECIMAL(15,2)) AS avg_ms FROM warehouse.ccdw_aggr_ocapi_request o WHERE $__timeFilter(o.request_date) GROUP BY o.client_id ORDER BY requests DESC LIMIT 25", + "format": 1 + } + ], + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto" + }, + "mappings": [] + }, + "overrides": [] + }, + "options": { + "showHeader": true, + "cellHeight": "sm", + "footer": { + "show": false + } + } + }, + { + "id": 7, + "title": "SCAPI Traffic & Latency (by endpoint)", + "type": "table", + "datasource": { + "type": "salesforce-b2c-cip-datasource", + "uid": "b2c-cip" + }, + "description": "scapi-traffic-latency (SCAPI data historical; widen range if empty)", + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 33 + }, + "targets": [ + { + "refId": "A", + "rawSql": "SELECT r.api_family, r.api_name, r.api_resource, SUM(r.num_requests) AS requests, CAST(SUM(r.response_time)/NULLIF(SUM(r.num_requests),0) AS DECIMAL(15,2)) AS avg_ms FROM warehouse.ccdw_aggr_scapi_request r WHERE $__timeFilter(r.request_date) GROUP BY r.api_family, r.api_name, r.api_resource ORDER BY requests DESC LIMIT 30", + "format": 1 + } + ], + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto" + }, + "mappings": [] + }, + "overrides": [] + }, + "options": { + "showHeader": true, + "cellHeight": "sm", + "footer": { + "show": false + } + } + }, + { + "id": 8, + "title": "SCAPI Cache Hit Ratio", + "type": "table", + "datasource": { + "type": "salesforce-b2c-cip-datasource", + "uid": "b2c-cip" + }, + "description": "scapi-cache-hit-ratio", + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 33 + }, + "targets": [ + { + "refId": "A", + "rawSql": "SELECT r.api_family, r.api_name, SUM(r.num_requests) AS requests, CAST(100.0*SUM(CASE WHEN r.cache_behavior='HIT' THEN r.num_requests ELSE 0 END)/NULLIF(SUM(CASE WHEN r.cache_behavior IN ('HIT','MISS') THEN r.num_requests ELSE 0 END),0) AS DECIMAL(15,2)) AS hit_ratio_pct FROM warehouse.ccdw_aggr_scapi_request r WHERE $__timeFilter(r.request_date) GROUP BY r.api_family, r.api_name ORDER BY requests DESC LIMIT 30", + "format": 1 + } + ], + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto" + }, + "mappings": [] + }, + "overrides": [] + }, + "options": { + "showHeader": true, + "cellHeight": "sm", + "footer": { + "show": false + } + } + } + ], + "refresh": "", + "schemaVersion": 39, + "tags": [ + "b2c", + "cip", + "technical" + ], + "templating": { + "list": [ + { + "name": "site", + "label": "Site", + "type": "query", + "datasource": { + "type": "salesforce-b2c-cip-datasource", + "uid": "b2c-cip" + }, + "query": { + "query": "sites" + }, + "definition": "sites", + "refresh": 1, + "sort": 1, + "includeAll": false, + "multi": false, + "current": { + "text": "Sites-NTOSFRA-Site", + "value": "Sites-NTOSFRA-Site", + "selected": true + }, + "options": [], + "description": "Storefront nsite_id, populated from CIP (distinct ccdw_dim_site.nsite_id).", + "hide": 0 + } + ] + }, + "time": { + "from": "now-90d", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "B2C Commerce \u2014 Technical / API Health (CIP)", + "uid": "b2c-cip-technical", + "version": 1, + "weekStart": "" +} \ No newline at end of file diff --git a/packages/b2c-grafana-datasource/dashboards/b2c-metrics-demo.json b/packages/b2c-grafana-datasource/dashboards/b2c-metrics-demo.json new file mode 100644 index 000000000..40eb0eee5 --- /dev/null +++ b/packages/b2c-grafana-datasource/dashboards/b2c-metrics-demo.json @@ -0,0 +1,739 @@ +{ + "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": 1, + "id": null, + "links": [], + "panels": [ + { + "datasource": { + "type": "salesforce-b2c-metrics-datasource", + "uid": "b2c-metrics-demo" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "tooltip": false, + "viz": false, + "legend": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "refId": "A", + "category": "overall", + "metricIds": [ + "totalCalls" + ], + "groupBy": [ + "apiFamily" + ], + "tenantId": "$tenant" + } + ], + "title": "Overall Total Calls", + "type": "timeseries" + }, + { + "datasource": { + "type": "salesforce-b2c-metrics-datasource", + "uid": "b2c-metrics-demo" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "tooltip": false, + "viz": false, + "legend": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "refId": "A", + "category": "scapi", + "metricIds": [ + "requestLatency" + ], + "groupBy": [ + "apiFamily" + ], + "tenantId": "$tenant" + } + ], + "title": "SCAPI Request Latency by API Family", + "type": "timeseries" + }, + { + "datasource": { + "type": "salesforce-b2c-metrics-datasource", + "uid": "b2c-metrics-demo" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "yellow", + "value": 0.5 + }, + { + "color": "green", + "value": 0.8 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "values": false, + "calcs": [ + "lastNotNull" + ], + "fields": "" + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "refId": "A", + "category": "scapi", + "metricIds": [ + "cacheHitRate" + ], + "groupBy": [ + "apiFamily", + "cacheStatus" + ], + "tenantId": "$tenant" + } + ], + "title": "SCAPI Cache Hit Rate", + "type": "stat" + }, + { + "datasource": { + "type": "salesforce-b2c-metrics-datasource", + "uid": "b2c-metrics-demo" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "tooltip": false, + "viz": false, + "legend": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 8 + }, + "id": 4, + "options": { + "legend": { + "calcs": [ + "sum" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "refId": "A", + "category": "ocapi", + "metricIds": [ + "totalCalls" + ], + "groupBy": [ + "ocapiCategory" + ], + "tenantId": "$tenant" + } + ], + "title": "OCAPI Calls by Category", + "type": "timeseries" + }, + { + "datasource": { + "type": "salesforce-b2c-metrics-datasource", + "uid": "b2c-metrics-demo" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "tooltip": false, + "viz": false, + "legend": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "2xx" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "4xx" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "5xx" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 8 + }, + "id": 5, + "options": { + "legend": { + "calcs": [ + "sum" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "refId": "A", + "category": "ecdn", + "metricIds": [ + "successAndError" + ], + "groupBy": [ + "statusClass", + "host" + ], + "tenantId": "$tenant" + } + ], + "title": "eCDN Responses by Status Class", + "type": "timeseries" + }, + { + "datasource": { + "type": "salesforce-b2c-metrics-datasource", + "uid": "b2c-metrics-demo" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "tooltip": false, + "viz": false, + "legend": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 6, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "refId": "A", + "category": "third-party", + "metricIds": [ + "callsP95" + ], + "groupBy": [ + "host" + ], + "tenantId": "$tenant" + } + ], + "title": "Third-Party Service Latency by Host", + "type": "timeseries" + }, + { + "datasource": { + "type": "salesforce-b2c-metrics-datasource", + "uid": "b2c-metrics-demo" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "tooltip": false, + "viz": false, + "legend": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 7, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "refId": "A", + "category": "controller", + "metricIds": [ + "callsMean" + ], + "groupBy": [ + "controller" + ], + "tenantId": "$tenant" + } + ], + "title": "Controller Latency by Pipeline", + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 39, + "tags": [ + "b2c-commerce", + "metrics", + "demo" + ], + "templating": { + "list": [ + { + "name": "tenant", + "label": "Tenant", + "type": "textbox", + "query": "bdpx_prd", + "current": { + "text": "bdpx_prd", + "value": "bdpx_prd" + }, + "options": [ + { + "text": "bdpx_prd", + "value": "bdpx_prd", + "selected": true + } + ], + "description": "Tenant id (same realm). e.g. bdpx_prd, bdpx_stg. Blank = datasource default.", + "hide": 0 + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "B2C Commerce Metrics \u2014 Demo", + "uid": "b2c-metrics-demo", + "version": 1, + "weekStart": "" +} \ No newline at end of file diff --git a/packages/b2c-grafana-datasource/demo/mock-metrics/Dockerfile b/packages/b2c-grafana-datasource/demo/mock-metrics/Dockerfile new file mode 100644 index 000000000..0b967cb83 --- /dev/null +++ b/packages/b2c-grafana-datasource/demo/mock-metrics/Dockerfile @@ -0,0 +1,18 @@ +FROM golang:1.26-alpine AS builder + +WORKDIR /build +COPY go.mod ./ +COPY main.go ./ + +RUN go build -o mock-metrics . + +FROM alpine:latest + +RUN apk --no-cache add ca-certificates + +WORKDIR /app +COPY --from=builder /build/mock-metrics . + +EXPOSE 8080 + +CMD ["./mock-metrics"] diff --git a/packages/b2c-grafana-datasource/demo/mock-metrics/README.md b/packages/b2c-grafana-datasource/demo/mock-metrics/README.md new file mode 100644 index 000000000..b5eccd575 --- /dev/null +++ b/packages/b2c-grafana-datasource/demo/mock-metrics/README.md @@ -0,0 +1,139 @@ +# Mock Metrics + OAuth Server + +A standalone Go HTTP server that provides synthetic time-series data for B2C Commerce Metrics API demo dashboards. + +## Endpoints + +### OAuth Token Endpoint +- **Path**: `POST /dwsso/oauth2/access_token` +- **Response**: Returns a well-formed JWT-ish token with `sfcc.metrics` scope +- **Example**: + ```bash + curl -X POST http://localhost:8080/dwsso/oauth2/access_token + ``` + +### Metrics Endpoint +- **Path**: `GET /observability/metrics/v1/organizations/{organizationId}/metrics/{category}` +- **Query Parameters**: + - `from` (required): Start time in epoch seconds + - `to` (required): End time in epoch seconds + - `apiFamily` (optional): Filter SCAPI metrics by API family + - `apiName` (optional): Filter SCAPI metrics by API name + - `ocapiCategory` (optional): Filter OCAPI metrics by category (shop/data) + - `ocapiApi` (optional): Filter OCAPI metrics by API + - `thirdPartyServiceId` (optional): Filter third-party metrics by service ID + +#### Supported Categories +- `overall` - Total calls across all services +- `sales` - Order revenue and counts +- `ecdn` - Edge CDN status counts +- `third-party` - Third-party service calls and latency +- `scapi` - SCAPI calls, status, cache, and latency +- `scapi-hooks` - SCAPI hook invocations and latency +- `mrt` - Lambda function invocations and duration +- `controller` - Controller invocation counts and latency +- `ocapi` - OCAPI calls and latency + +#### Example Requests + +```bash +# Overall metrics +curl 'http://localhost:8080/observability/metrics/v1/organizations/f_ecom_bdpx_prd/metrics/overall?from=1720000000&to=1720010000' + +# SCAPI metrics with filter +curl 'http://localhost:8080/observability/metrics/v1/organizations/f_ecom_bdpx_prd/metrics/scapi?from=1720000000&to=1720010000&apiFamily=product' + +# Third-party metrics +curl 'http://localhost:8080/observability/metrics/v1/organizations/f_ecom_bdpx_prd/metrics/third-party?from=1720000000&to=1720010000' + +# OCAPI metrics with filter +curl 'http://localhost:8080/observability/metrics/v1/organizations/f_ecom_bdpx_prd/metrics/ocapi?from=1720000000&to=1720010000&ocapiCategory=shop' +``` + +## Response Format + +All metrics endpoints return JSON with the following structure: + +```json +{ + "data": [ + { + "metricId": "totalCalls", + "title": "Total Calls", + "description": "Total API calls across all services", + "unit": "calls", + "dataSeries": [ + { + "id": "bdpx.total", + "name": "total", + "data": [ + {"timestamp": 1720000000, "value": 5234.5}, + {"timestamp": 1720000300, "value": 5421.2} + ] + } + ] + } + ] +} +``` + +## Synthetic Data + +The mock server generates synthetic time-series data with the following characteristics: + +- **Time Points**: ~1 data point every 5 minutes within the requested `[from, to]` window +- **Timestamps**: Epoch seconds (matching the real API format) +- **Values**: Deterministic but varied using sine waves with jitter to simulate realistic patterns +- **Series IDs**: Realistic packed format for tag enrichment (e.g., `bdpx.product HIT`, `2xx bdpx.host`) +- **Realm**: All series use realm `bdpx` + +### Series ID Patterns by Category + +- **SCAPI**: `bdpx.product`, `bdpx.custom`, `bdpx.product HIT`, `bdpx.product MISS`, `bdpx.product 2xx` +- **OCAPI**: `bdpx.shop`, `bdpx.data` +- **eCDN**: `2xx bdpx.host`, `3xx bdpx.host`, `4xx bdpx.host`, `5xx bdpx.host` +- **Third-Party**: `bdpx.login.salesforce.com`, `bdpx.api.stripe.com` +- **Controllers**: `bdpx.Product-Show`, `bdpx.Search-Show`, `bdpx.Cart-AddProduct` + +## Running the Server + +### Standalone (go run) +```bash +cd /Users/clavery/code/b2c-developer-tooling/feature-grafana/packages/b2c-grafana-datasource/demo/mock-metrics +go run . +``` + +### Build and Run +```bash +go build -o mock-metrics . +./mock-metrics +``` + +### Docker +```bash +docker build -t mock-metrics . +docker run -p 8080:8080 mock-metrics +``` + +### Docker Compose +The server is configured as the `mock-metrics` service in the parent demo's `docker-compose.yml`, accessible at `http://mock-metrics:8080` from other services. + +## Configuration + +The server listens on port `8080` by default. This is fixed in the code but can be changed in `main.go` if needed. + +## Use in Grafana Datasource + +Configure the B2C Metrics datasource with: + +```json +{ + "shortCode": "demo", + "tenantId": "f_ecom_bdpx_prd", + "clientId": "demo", + "apiUrl": "http://mock-metrics:8080/observability/metrics/v1", + "tokenUrl": "http://mock-metrics:8080/dwsso/oauth2/access_token" +} +``` + +With `secureJsonData.clientSecret = "demo"`. diff --git a/packages/b2c-grafana-datasource/demo/mock-metrics/go.mod b/packages/b2c-grafana-datasource/demo/mock-metrics/go.mod new file mode 100644 index 000000000..5d3e0b894 --- /dev/null +++ b/packages/b2c-grafana-datasource/demo/mock-metrics/go.mod @@ -0,0 +1,3 @@ +module mock-metrics + +go 1.26 diff --git a/packages/b2c-grafana-datasource/demo/mock-metrics/main.go b/packages/b2c-grafana-datasource/demo/mock-metrics/main.go new file mode 100644 index 000000000..687fb73ce --- /dev/null +++ b/packages/b2c-grafana-datasource/demo/mock-metrics/main.go @@ -0,0 +1,602 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "math" + "net/http" + "strconv" + "strings" +) + +const ( + realm = "bdpx" +) + +type OAuthResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + Scope string `json:"scope"` +} + +type DataPoint struct { + Timestamp int64 `json:"timestamp"` + Value float64 `json:"value"` +} + +type DataSeries struct { + ID string `json:"id"` + Name string `json:"name"` + Data []DataPoint `json:"data"` +} + +type Metric struct { + MetricID string `json:"metricId"` + Title string `json:"title"` + Description string `json:"description"` + Unit string `json:"unit"` + DataSeries []DataSeries `json:"dataSeries"` +} + +type MetricsResponse struct { + Data []Metric `json:"data"` +} + +func main() { + http.HandleFunc("/dwsso/oauth2/access_token", handleOAuth) + http.HandleFunc("/observability/metrics/v1/organizations/", handleMetrics) + // Liveness endpoint for container healthchecks (GET, so wget --spider succeeds). + http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok")) + }) + + port := ":8080" + log.Printf("Mock Metrics + OAuth server listening on %s", port) + log.Printf("OAuth endpoint: POST http://localhost%s/dwsso/oauth2/access_token", port) + log.Printf("Metrics endpoint: GET http://localhost%s/observability/metrics/v1/organizations/{orgId}/metrics/{category}", port) + if err := http.ListenAndServe(port, nil); err != nil { + log.Fatal(err) + } +} + +func handleOAuth(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Return a well-formed JWT-ish token (3 segments) + resp := OAuthResponse{ + AccessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkZW1vIiwic2NvcGUiOiJzZmNjLm1ldHJpY3MiLCJleHAiOjk5OTk5OTk5OTl9.mock_signature_for_demo", + TokenType: "Bearer", + ExpiresIn: 3600, + Scope: "sfcc.metrics", + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) +} + +func handleMetrics(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Parse path: /observability/metrics/v1/organizations/{orgId}/metrics/{category} + parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/observability/metrics/v1/organizations/"), "/") + if len(parts) < 3 || parts[1] != "metrics" { + http.Error(w, "Invalid path", http.StatusNotFound) + return + } + + category := parts[2] + query := r.URL.Query() + + fromStr := query.Get("from") + toStr := query.Get("to") + + if fromStr == "" || toStr == "" { + http.Error(w, "Missing from/to parameters", http.StatusBadRequest) + return + } + + from, err := strconv.ParseInt(fromStr, 10, 64) + if err != nil { + http.Error(w, "Invalid from parameter", http.StatusBadRequest) + return + } + + to, err := strconv.ParseInt(toStr, 10, 64) + if err != nil { + http.Error(w, "Invalid to parameter", http.StatusBadRequest) + return + } + + // Get filters + apiFamily := query.Get("apiFamily") + apiName := query.Get("apiName") + ocapiCategory := query.Get("ocapiCategory") + ocapiAPI := query.Get("ocapiApi") + thirdPartyServiceID := query.Get("thirdPartyServiceId") + + metrics := generateMetrics(category, from, to, apiFamily, apiName, ocapiCategory, ocapiAPI, thirdPartyServiceID) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(MetricsResponse{Data: metrics}) +} + +func generateMetrics(category string, from, to int64, apiFamily, apiName, ocapiCategory, ocapiAPI, thirdPartyServiceID string) []Metric { + switch category { + case "overall": + return generateOverallMetrics(from, to) + case "sales": + return generateSalesMetrics(from, to) + case "ecdn": + return generateECDNMetrics(from, to) + case "third-party": + return generateThirdPartyMetrics(from, to, thirdPartyServiceID) + case "scapi": + return generateSCAPIMetrics(from, to, apiFamily, apiName) + case "scapi-hooks": + return generateSCAPIHooksMetrics(from, to) + case "mrt": + return generateMRTMetrics(from, to) + case "controller": + return generateControllerMetrics(from, to) + case "ocapi": + return generateOCAPIMetrics(from, to, ocapiCategory, ocapiAPI) + default: + return []Metric{} + } +} + +func generateTimePoints(from, to int64) []int64 { + points := []int64{} + interval := int64(300) // 5 minutes + for t := from; t <= to; t += interval { + points = append(points, t) + } + // Ensure we have at least 2 points + if len(points) == 0 { + points = append(points, from, to) + } else if len(points) == 1 { + points = append(points, to) + } + return points +} + +func sineWithJitter(t int64, base, amplitude, period, jitter float64) float64 { + normalized := float64(t) / period + sine := base + amplitude*math.Sin(2*math.Pi*normalized) + jitterVal := (float64(t%100)/100.0 - 0.5) * jitter + return math.Max(0, sine+jitterVal) +} + +func generateOverallMetrics(from, to int64) []Metric { + points := generateTimePoints(from, to) + series := []DataSeries{ + { + ID: fmt.Sprintf("%s.total", realm), + Name: "total", + Data: generateData(points, func(t int64) float64 { + return sineWithJitter(t, 5000, 2000, 3600, 500) + }), + }, + { + ID: fmt.Sprintf("%s.success", realm), + Name: "success", + Data: generateData(points, func(t int64) float64 { + return sineWithJitter(t, 4800, 1900, 3600, 400) + }), + }, + { + ID: fmt.Sprintf("%s.error", realm), + Name: "error", + Data: generateData(points, func(t int64) float64 { + return sineWithJitter(t, 200, 100, 3600, 50) + }), + }, + } + + return []Metric{ + { + MetricID: "totalCalls", + Title: "Total Calls", + Description: "Total API calls across all services", + Unit: "calls", + DataSeries: series, + }, + } +} + +func generateSalesMetrics(from, to int64) []Metric { + points := generateTimePoints(from, to) + return []Metric{ + { + MetricID: "orderRevenue", + Title: "Order Revenue", + Description: "Total revenue from orders", + Unit: "USD", + DataSeries: []DataSeries{ + { + ID: fmt.Sprintf("%s.revenue", realm), + Name: "revenue", + Data: generateData(points, func(t int64) float64 { + return sineWithJitter(t, 50000, 20000, 7200, 5000) + }), + }, + }, + }, + { + MetricID: "orderCount", + Title: "Order Count", + Description: "Number of orders placed", + Unit: "orders", + DataSeries: []DataSeries{ + { + ID: fmt.Sprintf("%s.orders", realm), + Name: "orders", + Data: generateData(points, func(t int64) float64 { + return sineWithJitter(t, 500, 200, 7200, 50) + }), + }, + }, + }, + } +} + +func generateECDNMetrics(from, to int64) []Metric { + points := generateTimePoints(from, to) + series := []DataSeries{ + { + ID: fmt.Sprintf("2xx %s.host", realm), + Name: "2xx", + Data: generateData(points, func(t int64) float64 { + return sineWithJitter(t, 9500, 1000, 3600, 200) + }), + }, + { + ID: fmt.Sprintf("3xx %s.host", realm), + Name: "3xx", + Data: generateData(points, func(t int64) float64 { + return sineWithJitter(t, 300, 100, 3600, 30) + }), + }, + { + ID: fmt.Sprintf("4xx %s.host", realm), + Name: "4xx", + Data: generateData(points, func(t int64) float64 { + return sineWithJitter(t, 150, 50, 3600, 20) + }), + }, + { + ID: fmt.Sprintf("5xx %s.host", realm), + Name: "5xx", + Data: generateData(points, func(t int64) float64 { + return sineWithJitter(t, 50, 20, 3600, 10) + }), + }, + } + + return []Metric{ + { + MetricID: "statusCounts", + Title: "Status Counts", + Description: "Request count by status class", + Unit: "requests", + DataSeries: series, + }, + } +} + +func generateThirdPartyMetrics(from, to int64, serviceID string) []Metric { + points := generateTimePoints(from, to) + + // Default services if no filter + services := []string{ + fmt.Sprintf("%s.login.salesforce.com", realm), + fmt.Sprintf("%s.api.stripe.com", realm), + } + + if serviceID != "" { + services = []string{fmt.Sprintf("%s.%s", realm, serviceID)} + } + + var series []DataSeries + for _, svc := range services { + name := strings.TrimPrefix(svc, realm+".") + series = append(series, DataSeries{ + ID: svc, + Name: name, + Data: generateData(points, func(t int64) float64 { + return sineWithJitter(t, 100, 30, 3600, 10) + }), + }) + } + + return []Metric{ + { + MetricID: "thirdPartyCalls", + Title: "Third Party Calls", + Description: "Calls to third-party services", + Unit: "calls", + DataSeries: series, + }, + { + MetricID: "thirdPartyLatency", + Title: "Third Party Latency", + Description: "Latency for third-party service calls", + Unit: "ms", + DataSeries: generateLatencySeries(points, services), + }, + } +} + +func generateSCAPIMetrics(from, to int64, apiFamily, apiName string) []Metric { + points := generateTimePoints(from, to) + + families := []string{"product", "custom", "search"} + if apiFamily != "" { + families = []string{apiFamily} + } + + var callSeries []DataSeries + var cacheSeries []DataSeries + + for _, family := range families { + // Total calls per family → series id "bdpx.product" (familyOrStatus → apiFamily) + callSeries = append(callSeries, DataSeries{ + ID: fmt.Sprintf("%s.%s", realm, family), + Name: family, + Data: generateData(points, func(t int64) float64 { + return sineWithJitter(t, 1000, 400, 3600, 100) + }), + }) + + // Cache hit/miss → series id "bdpx.product HIT" (lastSpaceSplit → apiFamily + cacheStatus) + cacheSeries = append(cacheSeries, DataSeries{ + ID: fmt.Sprintf("%s.%s HIT", realm, family), + Name: fmt.Sprintf("%s HIT", family), + Data: generateData(points, func(t int64) float64 { + return sineWithJitter(t, 800, 300, 3600, 80) + }), + }) + cacheSeries = append(cacheSeries, DataSeries{ + ID: fmt.Sprintf("%s.%s MISS", realm, family), + Name: fmt.Sprintf("%s MISS", family), + Data: generateData(points, func(t int64) float64 { + return sineWithJitter(t, 200, 100, 3600, 20) + }), + }) + } + + // Response count by bare status class → series id "bdpx 2xx" (familyOrStatus → statusClass). + // The real API mixes these status-class series into the responseCount metric alongside + // per-family series; here we emit the status breakdown which drives the statusClass tag. + statusSeries := []DataSeries{ + {ID: fmt.Sprintf("%s 2xx", realm), Name: "2xx", Data: generateData(points, func(t int64) float64 { return sineWithJitter(t, 2800, 1000, 3600, 200) })}, + {ID: fmt.Sprintf("%s 4xx", realm), Name: "4xx", Data: generateData(points, func(t int64) float64 { return sineWithJitter(t, 120, 60, 3600, 15) })}, + {ID: fmt.Sprintf("%s 5xx", realm), Name: "5xx", Data: generateData(points, func(t int64) float64 { return sineWithJitter(t, 30, 15, 3600, 8) })}, + } + + // MetricIds MUST match the real Metrics API (and the tag-enrichment catalog): + // totalCalls (familyOrStatus), responseCount (familyOrStatus), cacheHitRate (lastSpaceSplit), + // requestLatency (familyOrOverallAgg). Using the real ids is what makes tag enrichment fire. + return []Metric{ + { + MetricID: "totalCalls", + Title: "SCAPI Total Calls", + Description: "Total SCAPI calls by API family", + Unit: "calls", + DataSeries: callSeries, + }, + { + MetricID: "responseCount", + Title: "SCAPI Responses by Status Class", + Description: "SCAPI responses by HTTP status class", + Unit: "calls", + DataSeries: statusSeries, + }, + { + MetricID: "cacheHitRate", + Title: "SCAPI Cache Hit/Miss", + Description: "SCAPI cache hit/miss counts by API family", + Unit: "calls", + DataSeries: cacheSeries, + }, + { + MetricID: "requestLatency", + Title: "SCAPI Request Latency", + Description: "SCAPI request latency by API family", + Unit: "ms", + DataSeries: generateLatencySeries(points, families), + }, + } +} + +func generateSCAPIHooksMetrics(from, to int64) []Metric { + points := generateTimePoints(from, to) + return []Metric{ + { + MetricID: "hookCalls", + Title: "Hook Calls", + Description: "SCAPI hook invocations", + Unit: "calls", + DataSeries: []DataSeries{ + { + ID: fmt.Sprintf("%s.before", realm), + Name: "before", + Data: generateData(points, func(t int64) float64 { + return sineWithJitter(t, 200, 80, 3600, 20) + }), + }, + { + ID: fmt.Sprintf("%s.after", realm), + Name: "after", + Data: generateData(points, func(t int64) float64 { + return sineWithJitter(t, 200, 80, 3600, 20) + }), + }, + }, + }, + { + MetricID: "hookLatency", + Title: "Hook Latency", + Description: "Hook execution time", + Unit: "ms", + DataSeries: []DataSeries{ + { + ID: fmt.Sprintf("%s.hooks", realm), + Name: "hooks", + Data: generateData(points, func(t int64) float64 { + return sineWithJitter(t, 50, 20, 3600, 10) + }), + }, + }, + }, + } +} + +func generateMRTMetrics(from, to int64) []Metric { + points := generateTimePoints(from, to) + return []Metric{ + { + MetricID: "mrtInvocations", + Title: "MRT Invocations", + Description: "Lambda function invocations", + Unit: "invocations", + DataSeries: []DataSeries{ + { + ID: fmt.Sprintf("%s.function1", realm), + Name: "function1", + Data: generateData(points, func(t int64) float64 { + return sineWithJitter(t, 150, 50, 3600, 15) + }), + }, + { + ID: fmt.Sprintf("%s.function2", realm), + Name: "function2", + Data: generateData(points, func(t int64) float64 { + return sineWithJitter(t, 100, 40, 3600, 10) + }), + }, + }, + }, + { + MetricID: "mrtDuration", + Title: "MRT Duration", + Description: "Function execution duration", + Unit: "ms", + DataSeries: []DataSeries{ + { + ID: fmt.Sprintf("%s.mrt", realm), + Name: "mrt", + Data: generateData(points, func(t int64) float64 { + return sineWithJitter(t, 250, 100, 3600, 30) + }), + }, + }, + }, + } +} + +func generateControllerMetrics(from, to int64) []Metric { + points := generateTimePoints(from, to) + controllers := []string{"Product-Show", "Search-Show", "Cart-AddProduct"} + + var series []DataSeries + for _, ctrl := range controllers { + series = append(series, DataSeries{ + ID: fmt.Sprintf("%s.%s", realm, ctrl), + Name: ctrl, + Data: generateData(points, func(t int64) float64 { + return sineWithJitter(t, 500, 200, 3600, 50) + }), + }) + } + + return []Metric{ + { + MetricID: "controllerCalls", + Title: "Controller Calls", + Description: "Controller invocation counts", + Unit: "calls", + DataSeries: series, + }, + { + MetricID: "controllerLatency", + Title: "Controller Latency", + Description: "Controller execution time", + Unit: "ms", + DataSeries: generateLatencySeries(points, controllers), + }, + } +} + +func generateOCAPIMetrics(from, to int64, ocapiCategory, ocapiAPI string) []Metric { + points := generateTimePoints(from, to) + + categories := []string{"shop", "data"} + if ocapiCategory != "" { + categories = []string{ocapiCategory} + } + + var series []DataSeries + for _, cat := range categories { + series = append(series, DataSeries{ + ID: fmt.Sprintf("%s.%s", realm, cat), + Name: cat, + Data: generateData(points, func(t int64) float64 { + return sineWithJitter(t, 800, 300, 3600, 80) + }), + }) + } + + return []Metric{ + { + MetricID: "ocapiCalls", + Title: "OCAPI Calls", + Description: "OCAPI call counts by category", + Unit: "calls", + DataSeries: series, + }, + { + MetricID: "ocapiLatency", + Title: "OCAPI Latency", + Description: "OCAPI request latency", + Unit: "ms", + DataSeries: generateLatencySeries(points, categories), + }, + } +} + +func generateData(points []int64, valueFunc func(int64) float64) []DataPoint { + data := make([]DataPoint, len(points)) + for i, t := range points { + data[i] = DataPoint{ + Timestamp: t, + Value: valueFunc(t), + } + } + return data +} + +func generateLatencySeries(points []int64, names []string) []DataSeries { + series := make([]DataSeries, len(names)) + for i, name := range names { + shortName := strings.TrimPrefix(name, realm+".") + series[i] = DataSeries{ + ID: fmt.Sprintf("%s.%s", realm, shortName), + Name: shortName, + Data: generateData(points, func(t int64) float64 { + // Latency: 50-300ms with sine wave and jitter + return sineWithJitter(t, 150, 100, 7200, 30) + }), + } + } + return series +} diff --git a/packages/b2c-grafana-datasource/docker-compose.real.yml b/packages/b2c-grafana-datasource/docker-compose.real.yml new file mode 100644 index 000000000..ec6953fa6 --- /dev/null +++ b/packages/b2c-grafana-datasource/docker-compose.real.yml @@ -0,0 +1,29 @@ +# Real-data override for the B2C Metrics demo stack. +# +# Usage (via Makefile, which injects credentials from the b2c CLI keychain): +# make real +# +# Or manually, with the five B2C_* vars exported in your shell: +# docker compose -f docker-compose.yml -f docker-compose.real.yml up -d --build grafana +# +# This override: +# - swaps the provisioning mount to provisioning-real/ (real datasource, no mock URLs) +# - passes the B2C_* credential env vars into the Grafana container for ${VAR} +# interpolation during provisioning +# - leaves the mock-metrics service defined but unused (bring up only `grafana`) +services: + grafana: + volumes: + - ./provisioning-real:/etc/grafana/provisioning:ro + - ./dashboards:/var/lib/grafana/dashboards:ro + environment: + # Credentials for real-mode datasource provisioning (interpolated into + # provisioning-real/datasources/b2c-metrics.yaml). Sourced from the host + # environment — the Makefile `real` target fills these from the b2c CLI. + B2C_SHORT_CODE: "${B2C_SHORT_CODE:?set B2C_SHORT_CODE (or use: make real)}" + B2C_TENANT_ID: "${B2C_TENANT_ID:?set B2C_TENANT_ID (or use: make real)}" + B2C_CLIENT_ID: "${B2C_CLIENT_ID:?set B2C_CLIENT_ID (or use: make real)}" + B2C_CLIENT_SECRET: "${B2C_CLIENT_SECRET:?set B2C_CLIENT_SECRET (or use: make real)}" + B2C_ACCOUNT_MANAGER_HOST: "${B2C_ACCOUNT_MANAGER_HOST:-account.demandware.com}" + # No dependency on the mock in real mode. + depends_on: !reset [] diff --git a/packages/b2c-grafana-datasource/docker-compose.yml b/packages/b2c-grafana-datasource/docker-compose.yml new file mode 100644 index 000000000..b01213774 --- /dev/null +++ b/packages/b2c-grafana-datasource/docker-compose.yml @@ -0,0 +1,59 @@ +services: + # Mock Metrics API server (OAuth + Metrics endpoints with synthetic data) + mock-metrics: + build: + context: ./demo/mock-metrics + dockerfile: Dockerfile + container_name: b2c-mock-metrics + # Host mapping is only for optional debugging; Grafana reaches the mock over the + # compose network at mock-metrics:8080. Published on 18080 to avoid colliding with + # anything already bound to 8080 on the host. + ports: + - "18080:8080" + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/healthz"] + interval: 5s + timeout: 3s + retries: 3 + start_period: 5s + networks: + - grafana-net + + # Grafana with embedded B2C Metrics datasource plugin + grafana: + build: + context: ../ # Build from packages/ dir to access both plugin + SDK + dockerfile: b2c-grafana-datasource/Dockerfile + container_name: b2c-grafana + ports: + - "3000:3000" + environment: + # Allow unsigned plugins (both datasource types) + GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS: "salesforce-b2c-metrics-datasource,salesforce-b2c-cip-datasource" + # Anonymous admin for demo + GF_AUTH_ANONYMOUS_ENABLED: "true" + GF_AUTH_ANONYMOUS_ORG_ROLE: "Admin" + GF_SECURITY_ADMIN_USER: "admin" + GF_SECURITY_ADMIN_PASSWORD: "admin" + # Provisioning + GF_PATHS_PROVISIONING: "/etc/grafana/provisioning" + volumes: + # Mount provisioning configs + - ./provisioning:/etc/grafana/provisioning:ro + # Mount dashboards + - ./dashboards:/var/lib/grafana/dashboards:ro + depends_on: + mock-metrics: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + networks: + - grafana-net + +networks: + grafana-net: + driver: bridge diff --git a/packages/b2c-grafana-datasource/docs/api-reference.md b/packages/b2c-grafana-datasource/docs/api-reference.md new file mode 100644 index 000000000..5ff71e35a --- /dev/null +++ b/packages/b2c-grafana-datasource/docs/api-reference.md @@ -0,0 +1,461 @@ +# API Reference + +Reference for CallResource endpoints exposed by the B2C Commerce datasources. + +## Overview + +Grafana datasource plugins expose HTTP resource endpoints via the `CallResource` mechanism. These endpoints are used by the frontend (query editor, variable editor) to fetch dynamic data like available categories, filters, schema metadata, etc. + +**Base URL**: `/api/datasources/proxy/uid/{datasource-uid}/` + +## Metrics Datasource Endpoints + +### GET /categories + +Returns the list of available metric categories for dropdowns and template variables. + +**Request**: +```http +GET /api/datasources/proxy/uid/b2c-metrics-prod/categories +``` + +**Response** (200 OK): +```json +[ + {"label": "Overall", "value": "overall"}, + {"label": "Sales", "value": "sales"}, + {"label": "eCDN", "value": "ecdn"}, + {"label": "Third-party", "value": "third-party"}, + {"label": "SCAPI", "value": "scapi"}, + {"label": "SCAPI Hooks", "value": "scapi-hooks"}, + {"label": "MRT", "value": "mrt"}, + {"label": "Controller", "value": "controller"}, + {"label": "OCAPI", "value": "ocapi"} +] +``` + +**Fields**: +- `label`: Human-readable category name (for UI display) +- `value`: Category identifier (used in query model) + +**Notes**: +- Static list (no API call to backend services) +- Matches the 9 Metrics API endpoints + +--- + +### GET /filters + +Returns the applicable server filters for a given category. + +**Request**: +```http +GET /api/datasources/proxy/uid/b2c-metrics-prod/filters?category=scapi +``` + +**Query Parameters**: +- `category` (required): Category identifier (e.g., `scapi`, `ocapi`, `third-party`) + +**Response** (200 OK) for `category=scapi`: +```json +[ + { + "name": "apiFamily", + "label": "API Family", + "placeholder": "product, checkout, etc." + }, + { + "name": "apiName", + "label": "API Name", + "placeholder": "shopper-products, etc." + }, + { + "name": "apiVersion", + "label": "API Version", + "placeholder": "v1, v2, etc." + } +] +``` + +**Response** (200 OK) for `category=ocapi`: +```json +[ + { + "name": "ocapiCategory", + "label": "OCAPI Category", + "placeholder": "shop, data" + }, + { + "name": "ocapiApi", + "label": "OCAPI API", + "placeholder": "" + } +] +``` + +**Response** (200 OK) for `category=third-party`: +```json +[ + { + "name": "thirdPartyServiceId", + "label": "Service ID", + "placeholder": "External service identifier" + } +] +``` + +**Response** (200 OK) for `category=overall` (no filters): +```json +[] +``` + +**Fields**: +- `name`: Filter field name (used in query model) +- `label`: Human-readable label for UI +- `placeholder`: Example values for help text + +**Notes**: +- Static mapping (no API call) +- Categories without server filters return empty array + +--- + +### GET /metrics + +Returns available metrics for a given category. + +**Request**: +```http +GET /api/datasources/proxy/uid/b2c-metrics-prod/metrics?category=scapi +``` + +**Query Parameters**: +- `category` (required): Category identifier + +**Response** (200 OK): +```json +[ + {"label": "Total Calls", "value": "scapi.totalCalls", "unit": "requests/sec"}, + {"label": "Cache Hit Rate", "value": "scapi.cacheHitRate", "unit": "percent"}, + {"label": "P95 Latency", "value": "scapi.p95Latency", "unit": "ms"} +] +``` + +**Fields**: +- `label`: Human-readable metric name +- `value`: Metric identifier (as returned by API) +- `unit`: Measurement unit (for display) + +**Notes**: +- Performs a "probe" API call with 5-minute window +- Caches result for 5 minutes +- May return empty array if category has no data for this tenant + +**Error Response** (500 Internal Server Error): +```json +{ + "error": "Failed to fetch metrics: 403 Forbidden" +} +``` + +--- + +### GET /label-keys + +Returns available label keys for filtering and grouping. + +**Request**: +```http +GET /api/datasources/proxy/uid/b2c-metrics-prod/label-keys?category=scapi +``` + +**Query Parameters**: +- `category` (required): Category identifier + +**Response** (200 OK): +```json +[ + {"label": "API Family", "value": "apiFamily"}, + {"label": "API Name", "value": "apiName"}, + {"label": "Cache Status", "value": "cacheStatus"}, + {"label": "Status Class", "value": "statusClass"}, + {"label": "Realm", "value": "realm"}, + {"label": "Environment", "value": "environment"} +] +``` + +**Fields**: +- `label`: Human-readable label key name +- `value`: Label key identifier (as enriched by SDK) + +**Notes**: +- Based on tag catalog (static + category-specific) +- Used for label filters and group-by dropdowns + +--- + +### GET /label-values + +Returns observed values for a given label key. + +**Request**: +```http +GET /api/datasources/proxy/uid/b2c-metrics-prod/label-values?category=scapi&labelKey=apiFamily +``` + +**Query Parameters**: +- `category` (required): Category identifier +- `labelKey` (required): Label key to fetch values for + +**Response** (200 OK): +```json +["product", "checkout", "customer", "search"] +``` + +**Notes**: +- Performs a "probe" API call with 5-minute window +- Extracts unique values from series labels +- Caches result for 5 minutes +- May be empty if no data in window + +**Error Response** (400 Bad Request): +```json +{ + "error": "Missing required parameter: labelKey" +} +``` + +--- + +## CIP Datasource Endpoints + +### GET /tables + +Returns list of available tables in the CIP warehouse. + +**Request**: +```http +GET /api/datasources/proxy/uid/b2c-cip-prod/tables +``` + +**Response** (200 OK): +```json +[ + {"name": "orders", "type": "TABLE"}, + {"name": "order_line_items", "type": "TABLE"}, + {"name": "products", "type": "TABLE"}, + {"name": "customers", "type": "TABLE"} +] +``` + +**Fields**: +- `name`: Table name (use in FROM clause) +- `type`: Object type (TABLE, VIEW) + +**Notes**: +- Queries CIP metadata via `getTables()` JDBC metadata call +- Cached for 15 minutes +- Authenticated (requires valid OAuth token) + +**Error Response** (500 Internal Server Error): +```json +{ + "error": "Failed to fetch tables: connection failed" +} +``` + +--- + +### GET /columns + +Returns columns for a given table. + +**Request**: +```http +GET /api/datasources/proxy/uid/b2c-cip-prod/columns?table=orders +``` + +**Query Parameters**: +- `table` (required): Table name + +**Response** (200 OK): +```json +[ + {"name": "order_id", "type": "STRING"}, + {"name": "submit_date", "type": "TIMESTAMP"}, + {"name": "revenue", "type": "DECIMAL"}, + {"name": "site_id", "type": "STRING"}, + {"name": "customer_id", "type": "STRING"} +] +``` + +**Fields**: +- `name`: Column name +- `type`: SQL data type (STRING, INTEGER, DECIMAL, TIMESTAMP, etc.) + +**Notes**: +- Queries CIP metadata via `getColumns()` JDBC metadata call +- Cached for 15 minutes per table +- Used by schema browser + +**Error Response** (400 Bad Request): +```json +{ + "error": "Missing required parameter: table" +} +``` + +--- + +### POST /query-preview + +Validates a SQL query without executing it (dry-run). + +**Request**: +```http +POST /api/datasources/proxy/uid/b2c-cip-prod/query-preview +Content-Type: application/json + +{ + "query": "SELECT COUNT(*) FROM orders WHERE $__timeFilter(submit_date)" +} +``` + +**Response** (200 OK): +```json +{ + "valid": true, + "expandedQuery": "SELECT COUNT(*) FROM orders WHERE submit_date >= TIMESTAMP '2026-07-13 00:00:00' AND submit_date < TIMESTAMP '2026-07-14 00:00:00'" +} +``` + +**Response** (200 OK with errors): +```json +{ + "valid": false, + "error": "Syntax error: unexpected token 'FRUM' at line 1, column 14" +} +``` + +**Fields**: +- `valid`: Whether query passes syntax validation +- `expandedQuery`: Query with macros expanded (if valid) +- `error`: Error message (if invalid) + +**Notes**: +- Expands Grafana macros (`$__timeFilter`, etc.) +- Does NOT execute query (no data returned) +- Useful for query editor validation + +--- + +## Common HTTP Status Codes + +### Success Codes + +- **200 OK**: Request succeeded, response body contains data +- **204 No Content**: Request succeeded, no response body + +### Client Error Codes + +- **400 Bad Request**: Missing or invalid query parameters +- **401 Unauthorized**: OAuth token missing or invalid (should not reach frontend — datasource not configured) +- **403 Forbidden**: OAuth token lacks required scopes +- **404 Not Found**: Resource not found (wrong endpoint) + +### Server Error Codes + +- **500 Internal Server Error**: Backend error (API call failed, parsing error, etc.) +- **503 Service Unavailable**: Downstream service (Metrics API, CIP) unavailable + +## Error Response Format + +All error responses follow this structure: + +```json +{ + "error": "Human-readable error message" +} +``` + +The `error` field contains a user-friendly message (safe to display in UI). + +## Authentication + +All CallResource requests are authenticated via the datasource's configured OAuth credentials: + +1. Grafana includes datasource settings (client ID/secret) in backend context +2. Backend acquires OAuth token (cached, auto-refreshed) +3. Backend calls downstream API (Metrics API, CIP) with token +4. Backend returns response to frontend + +Frontend never handles credentials directly. + +## Rate Limiting + +**Current behavior** (as of v0.1.0): +- No rate limiting on CallResource endpoints +- Discovery endpoints (`/metrics`, `/label-values`, `/tables`, `/columns`) can amplify API calls +- Future versions will add caching, singleflight, and backoff + +**Best practices**: +- Don't poll CallResource endpoints in tight loops +- Rely on backend caching (5–15 minute TTLs) +- Implement debouncing in frontend (e.g., 300ms delay after typing) + +## Caching Behavior + +### Metrics Datasource + +| Endpoint | Cache Duration | Cache Key | +|---|---|---| +| `/categories` | N/A (static) | N/A | +| `/filters` | N/A (static) | N/A | +| `/metrics` | 5 minutes | `category` | +| `/label-keys` | N/A (static per category) | N/A | +| `/label-values` | 5 minutes | `category:labelKey` | + +### CIP Datasource + +| Endpoint | Cache Duration | Cache Key | +|---|---|---| +| `/tables` | 15 minutes | `datasource-uid` | +| `/columns` | 15 minutes | `datasource-uid:table` | +| `/query-preview` | None (always validates) | N/A | + +Cache is in-memory (per plugin instance). Restarting Grafana clears cache. + +## Debugging CallResource Requests + +### Via Browser DevTools + +1. Open Grafana in browser +2. Open DevTools → Network tab +3. Filter by `datasources/proxy` +4. Inspect request/response + +**Example**: +``` +Request URL: http://localhost:3000/api/datasources/proxy/uid/b2c-metrics-demo/categories +Status: 200 OK +Response: [{"label":"Overall","value":"overall"},...] +``` + +### Via Grafana Logs + +Backend logs CallResource requests at debug level: + +```bash +docker compose logs -f grafana | grep CallResource +``` + +**Example log**: +``` +level=debug msg="CallResource request" path=/categories datasource=b2c-metrics-demo +level=debug msg="CallResource response" path=/categories status=200 duration=2ms +``` + +## Next Steps + +- **Query Editor**: Use these endpoints in [Query Editor Guide](./query-editor.md) +- **Architecture**: Understand request flow in [Architecture Guide](./architecture.md) diff --git a/packages/b2c-grafana-datasource/docs/architecture.md b/packages/b2c-grafana-datasource/docs/architecture.md new file mode 100644 index 000000000..921ff516a --- /dev/null +++ b/packages/b2c-grafana-datasource/docs/architecture.md @@ -0,0 +1,588 @@ +# Architecture Guide + +Technical architecture of the B2C Commerce Grafana datasources. + +## System Overview + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Grafana UI │ +│ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ │ +│ │ Config │ │ Query │ │ DataSource │ │ +│ │ Editor │ │ Editor │ │ API │ │ +│ │ (Settings) │ │ (Query UI) │ │ (Bridge) │ │ +│ └─────────────┘ └─────────────┘ └──────────────┘ │ +└────────────────────────────┬────────────────────────────────────┘ + │ JSON-RPC over HTTP/gRPC +┌────────────────────────────┴────────────────────────────────────┐ +│ Grafana Backend Plugins (Go) │ +│ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ Metrics Plugin (gpx_b2c_metrics) │ │ +│ │ - pkg/plugin/datasource.go │ │ +│ │ ├── QueryData(): Execute metric queries │ │ +│ │ ├── CheckHealth(): Validate connectivity │ │ +│ │ └── CallResource(): /categories, /filters, etc. │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ CIP Plugin (gpx_b2c_cip) │ │ +│ │ - pkg/cip/plugin/datasource.go │ │ +│ │ ├── QueryData(): Execute SQL queries (via sqlds/v4) │ │ +│ │ ├── CheckHealth(): Validate CIP connection │ │ +│ │ └── CallResource(): /tables, /columns, etc. │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ B2C Tooling SDK Go (shared, via replace directive) │ │ +│ │ │ │ +│ │ ├── auth.OAuthStrategy │ │ +│ │ │ - Client credentials flow │ │ +│ │ │ - Token caching (scope + expiry) │ │ +│ │ │ - Auto-refresh on 401 │ │ +│ │ │ │ │ +│ │ ├── clients/metrics.Client │ │ +│ │ │ - 9 category endpoints (GetOverallMetrics, etc.) │ │ +│ │ │ - Auto-adds sfcc.metrics + tenant scope │ │ +│ │ │ - Normalizes timestamps (seconds → milliseconds) │ │ +│ │ │ - Enriches series with tags │ │ +│ │ │ │ │ +│ │ ├── clients/cip.Client │ │ +│ │ │ - JDBC-over-Avatica connection │ │ +│ │ │ - Sticky session handling │ │ +│ │ │ - Query execution + metadata │ │ +│ │ │ │ │ +│ │ └── operations/metrics │ │ +│ │ ├── ResolveMetricsWindow() │ │ +│ │ │ - 30-day retention enforcement │ │ +│ │ │ - 24-hour default window │ │ +│ │ │ │ │ +│ │ └── ParseSeriesTags() │ │ +│ │ - Extracts structured tags from series IDs │ │ +│ │ - 32 golden test cases (parity with TS SDK) │ │ +│ └──────────────────────────────────────────────────────────┘ │ +└────────────────────────────┬────────────────────────────────────┘ + │ HTTPS +┌────────────────────────────┴────────────────────────────────────┐ +│ Backend APIs │ +│ │ +│ account.demandware.com │ +│ ├── POST /dwsso/oauth2/access_token │ +│ │ - Client credentials grant │ +│ │ - Returns access_token + expires_in │ +│ │ │ +│ {shortCode}.api.commercecloud.salesforce.com │ +│ └── GET /observability/metrics/v1/organizations/{orgId}/... │ +│ ├── /metrics/overall │ +│ ├── /metrics/scapi │ +│ └── ... (9 categories) │ +│ │ +│ cip-{region}.commercecloud.salesforce.com │ +│ └── POST /avatica │ +│ - Apache Avatica JSON protocol │ +│ - Calcite SQL execution │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Plugin Architecture + +### Two Independent Datasources + +The B2C Commerce plugin packages two separate Grafana datasources: + +1. **Metrics Datasource** (`salesforce-b2c-metrics-datasource`) + - Plugin ID: `salesforce-b2c-metrics-datasource` + - Backend binary: `gpx_b2c_metrics` + - Source: `pkg/plugin/`, `src/` + - Purpose: Time-series metrics from Metrics API + +2. **CIP Datasource** (`salesforce-b2c-cip-datasource`) + - Plugin ID: `salesforce-b2c-cip-datasource` + - Backend binary: `gpx_b2c_cip` + - Source: `pkg/cip/plugin/`, `src/cip/` + - Purpose: Raw SQL queries against CIP warehouse + +Both share: +- The same Go SDK (auth, clients, operations) +- Similar OAuth configuration UI +- Separate registration, configuration, query models + +### Frontend Architecture + +**Metrics Frontend** (`src/`): +- `ConfigEditor.tsx`: Datasource settings form +- `QueryEditor.tsx`: Tiered filter interface +- `datasource.ts`: DataSourceApi implementation +- `types.ts`: TypeScript interfaces +- `module.ts`: Plugin registration + +**CIP Frontend** (`src/cip/`): +- `ConfigEditor.tsx`: CIP settings form +- `QueryEditor.tsx`: SQL editor + schema browser +- `VariableQueryEditor.tsx`: Variable query interface +- `datasource.ts`: DataSourceApi implementation +- `types.ts`: TypeScript interfaces +- `module.ts`: Plugin registration + +Both frontends are built with: +- React + TypeScript +- Grafana plugin SDK (`@grafana/data`, `@grafana/ui`, `@grafana/runtime`) +- Webpack bundling (dual-entry: `module.ts` + `cip/module.ts`) + +### Backend Architecture + +**Metrics Backend** (`pkg/plugin/`): +- `main.go`: Plugin entry point (21 lines) +- `datasource.go`: Core logic (405 lines) + - `NewDatasource()`: Factory, parses settings + - `QueryData()`: Executes metric queries + - `CheckHealth()`: Validates connectivity + - `CallResource()`: Serves /categories, /filters, /metrics, /label-keys, /label-values + +**CIP Backend** (`pkg/cip/plugin/`): +- `main.go`: Plugin entry point +- `datasource.go`: Core logic (uses grafana/sqlds/v4) + - Inherits QueryData from sqlds.Driver + - `CheckHealth()`: Pings CIP with SELECT 1 + - `CallResource()`: Serves /tables, /columns +- `macros.go`: SQL macro expansion (`$__timeFilter`, `$__timeGroup`, etc.) +- `driver.go`: sqlds.Driver implementation (opens connections, executes queries) + +Both backends use: +- `grafana-plugin-sdk-go/backend`: Plugin framework +- `b2c-tooling-sdk-go`: OAuth + API clients + +## Data Flow + +### Metrics Query Execution + +1. **User Action**: User configures query in dashboard + - Selects category (e.g., `scapi`) + - Sets filters (e.g., `apiFamily=product`) + - Sets time range in Grafana UI + +2. **Frontend → Backend**: Grafana sends `QueryDataRequest` + ```json + { + "queries": [{ + "refId": "A", + "category": "scapi", + "apiFamily": "product", + "timeRange": { + "from": "2026-07-13T12:00:00Z", + "to": "2026-07-14T12:00:00Z" + } + }] + } + ``` + +3. **Backend Processing** (`QueryData()`): + - Parse query JSON → `QueryModel` + - Resolve time window (enforce 30-day retention, default 24h) + - Build filter map from query model + - Route to SDK client method based on category + - SDK handles OAuth (fetch/cache token) + - SDK calls Metrics API + - SDK normalizes response (timestamps ×1000, tag extraction) + - Convert to Grafana frames + +4. **Frame Construction**: + - For each metric in response: + - For each series in metric: + - Create `data.Frame` with: + - Time field: `[]time.Time` + - Value field: `[]float64` with labels from tags + - Field config: unit, display name + - Add metadata: executed query, notices + +5. **Backend → Frontend**: Return `QueryDataResponse` + ```go + backend.DataResponse{ + Frames: []{ + { + Name: "SCAPI Total Calls (product, bdpx/prd)", + Fields: [ + {Name: "time", Values: [...]}, + {Name: "scapi.totalCalls", Labels: {...}, Values: [...]} + ] + } + } + } + ``` + +6. **Grafana Rendering**: UI renders frames as time series + +### CIP Query Execution + +1. **User Action**: User writes SQL query with macros + +2. **Frontend → Backend**: Grafana sends `QueryDataRequest` with SQL + +3. **Backend Processing** (via `grafana/sqlds/v4`): + - Expand macros: `$__timeFilter` → `WHERE submit_date >= ... AND submit_date < ...` + - Open CIP connection (via SDK's `cip.Client`) + - Execute query via JDBC-over-Avatica + - Fetch rows + metadata + - Convert to Grafana frames: + - Time series: Requires `time` column + numeric values + - Table: Any column structure + +4. **Frame Construction**: + - `sqlds` handles conversion (rows → frames) + - Backend adds: + - Field types (time, number, string) + - Display names + - Metadata (executed query) + +5. **Backend → Frontend**: Return `QueryDataResponse` + +6. **Grafana Rendering**: UI renders as time series or table + +## Configuration Flow + +### Datasource Initialization + +1. **User Action**: Add datasource in Grafana UI + - Enters shortCode, tenantId in `jsonData` (plaintext) + - Enters clientId, clientSecret in `secureJsonData` (encrypted) + +2. **Backend Initialization** (`NewDatasource()`): + - Parse settings from `DataSourceInstanceSettings` + - Validate required fields + - Create `auth.OAuthStrategy` with credentials + - Create `metrics.Client` or `cip.Client` with config + - SDK auto-adds required scopes: + - Metrics: `sfcc.metrics` + `SALESFORCE_COMMERCE_API:{tenantId}` + - CIP: CIP scope + tenant scope + +3. **Health Check** (`CheckHealth()`): + - **Metrics**: Fetch last 5 minutes of "overall" metrics + - **CIP**: Execute `SELECT 1` test query + - Return success or error message + +### Security Model + +**Credential Storage**: +- `jsonData` (visible in Grafana UI): shortCode, tenantId, accountManagerHost +- `secureJsonData` (encrypted at rest): clientId, clientSecret + - Encrypted in Grafana database + - Decrypted server-side into `DecryptedSecureJSONData` map + - Never exposed to frontend + +**Token Management**: +- OAuth tokens in-memory only (not persisted) +- Cache key: `host:clientId:method:scopes` (sorted, comma-joined) +- Reused until `expires_in - 60s` margin +- On 401 after prior success: invalidate + retry once +- Typical lifetime: ~3600s + +**Network Security**: +- All calls over HTTPS (enforced by Go http.Client) +- TLS 1.2+ required +- OAuth client credentials flow (no user password exposure) +- Scopes enforce least-privilege access + +## Performance Characteristics + +### Token Caching + +- **Cold start**: 1 token fetch per datasource instance +- **Warm state**: Token reused for ~3600s +- **Concurrent queries**: Single-flight token fetch (golang.org/x/oauth2) +- **Memory**: ~2KB per cached token + +### HTTP Connection Pooling + +- Go's default `http.Client` pools connections +- Typical: 100 idle connections, 90s keep-alive +- Reused across all queries to same host + +### Query Performance + +**Metrics**: +- **Small query** (5m window): ~200ms (token + API + transform) +- **Large query** (30d window): ~2-5s (depends on cardinality) +- **Parallel queries**: Plugin is goroutine-safe + +**CIP**: +- **Simple aggregation** (1h window): ~500ms +- **Large scan** (13m history): ~5-30s (depends on table size) +- **Connection pooling**: CIP connections are session-scoped (sticky) + +### Data Transfer + +**Metrics**: +- Typical series: 1-10 data points per minute +- 24-hour query: ~1440 points per series +- JSON response: ~100 bytes per point +- Frame serialization: ~50% overhead (Grafana Arrow format) + +**CIP**: +- Depends on query (SELECT * → large, aggregations → small) +- Row limit enforced (default 1000 rows for unbounded queries) +- DECIMAL columns transmitted as strings (Avatica limitation) + +## Error Handling + +### OAuth Errors + +- **401 Unauthorized**: Invalid credentials → surface to user +- **403 Forbidden**: Insufficient scope → surface to user +- **Network timeout**: Retry with backoff (handled by SDK) + +### Metrics API Errors + +- **400 Bad Request**: Invalid query params → parse error message +- **404 Not Found**: Wrong endpoint → check shortCode/tenantId +- **429 Rate Limited**: Too many requests → surface retry-after +- **500 Internal Server Error**: API issue → log + surface to user + +### CIP Errors + +- **SQL syntax error**: Calcite validation error → surface to user +- **Connection refused**: CIP unavailable → surface to user +- **Query timeout**: Long-running query → suggest adding LIMIT + +### Time Window Errors + +- **Beyond retention** (Metrics): Clamp to 30 days + add warning notice +- **Invalid range**: from > to → return error + +## Backend Contract + +This section defines the contract between the React frontend and the Go backend. + +### Metrics Query Model + +The frontend sends queries with this structure: + +```typescript +interface B2CMetricsQuery extends DataQuery { + category: string; // Required: 'overall', 'scapi', etc. + metrics: string[]; // Optional: metric IDs to fetch + apiFamily?: string; // SCAPI filter + apiName?: string; // SCAPI filter + apiVersion?: string; // SCAPI filter + ocapiCategory?: string; // OCAPI filter + ocapiApi?: string; // OCAPI filter + thirdPartyServiceId?: string; // Third-party filter + labelFilters?: LabelFilter[]; // Post-fetch filtering + groupBy?: string[]; // Label keys to group by + format: 0 | 1; // 0=time series, 1=table +} +``` + +**Backend receives**: Unmarshaled from `backend.DataQuery.JSON` + +### CIP Query Model + +```typescript +interface B2CCIPQuery extends DataQuery { + rawSql: string; // SQL query with macros + format: 0 | 1; // 0=time series, 1=table + timeColumn?: string; // Column with timestamps + metricColumn?: string; // Column with values + fillMode?: 'null' | 'previous' | 'value'; +} +``` + +**Backend receives**: Unmarshaled, then macros expanded before execution + +### Configuration Model + +**JSON Data** (plaintext): +```typescript +interface DataSourceOptions { + shortCode: string; // Instance short code + tenantId: string; // Tenant ID + accountManagerHost?: string; // Optional, defaults to account.demandware.com + // CIP-specific: + cipHost?: string; // CIP endpoint (for CIP datasource) +} +``` + +**Secure JSON Data** (encrypted): +```typescript +interface SecureJsonData { + clientId: string; // OAuth client ID + clientSecret: string; // OAuth client secret +} +``` + +**Backend receives**: Via `backend.DataSourceInstanceSettings` +- `JSONData`: Unmarshaled from jsonData +- `DecryptedSecureJSONData`: Map with decrypted secrets + +### Response Format + +**Success**: +```go +backend.DataResponse{ + Frames: data.Frames{...}, + Error: nil, +} +``` + +**Error**: +```go +backend.DataResponse{ + Frames: nil, + Error: fmt.Errorf("user-friendly error message"), +} +``` + +## Testing Strategy + +### Unit Tests (SDK level) + +- `auth/oauth_test.go`: OAuth flow with mock token server +- `operations/metrics/window_test.go`: Window resolution edge cases +- `operations/metrics/tags_golden_test.go`: Tag extraction (32 cases) +- `clients/metrics/*_test.go`: Metrics client + partitioning +- `clients/cip/*_test.go`: CIP client + sticky sessions + +### Integration Tests (Plugin level) + +- Mock Metrics API with `httptest.NewServer` +- Verify QueryData produces correct frames +- Test CheckHealth success/failure paths +- Validate CallResource routes + +### Manual Testing + +- Local Grafana instance with plugin installed +- Demo mode (mock server) +- Real mode (live B2C credentials) +- Verify dashboards render correctly +- Test all 9 categories + filters + +## Deployment Architecture + +### Development + +``` +Developer Machine +├── Go 1.26 toolchain +├── Grafana 9.0+ (local or Docker) +└── Plugin source code + - Edit Go code + - `go build ./pkg` + `go build ./pkg/cip` + - Copy to Grafana plugins dir + - Restart Grafana +``` + +### Production + +``` +Grafana Server (Linux) +├── /var/lib/grafana/plugins/salesforce-b2c-metrics-datasource/ +│ ├── dist/gpx_b2c_metrics_linux_amd64 (binary) +│ └── src/plugin.json (metadata) +├── /var/lib/grafana/plugins/salesforce-b2c-cip-datasource/ +│ ├── dist/gpx_b2c_cip_linux_amd64 (binary) +│ └── src/cip/plugin.json (metadata) +├── grafana.ini +│ - allow_loading_unsigned_plugins = salesforce-b2c-metrics-datasource,salesforce-b2c-cip-datasource +└── Grafana process (systemd) + - Loads plugins at startup + - Spawns binaries as subprocesses + - Communicates via gRPC/JSON-RPC +``` + +### Cloud Grafana + +- Upload signed plugin to Grafana Cloud +- Or use private plugin registry +- Binary built with cross-compilation for multiple platforms + +## Dependencies + +### Direct Dependencies + +- `grafana-plugin-sdk-go` v0.257.0 (plugin framework) +- `grafana/sqlds` v4 (CIP SQL driver framework) +- `b2c-tooling-sdk-go` (local monorepo, via replace directive) + +### Transitive Dependencies + +- `golang.org/x/oauth2` (OAuth client) +- `google.golang.org/grpc` (plugin communication) +- `github.com/apache/arrow/go/v15` (data frames) + +### Build Dependencies + +- Go 1.26 toolchain +- Node.js + npm (for frontend build) +- Docker (for demo/testing) + +## File Manifest + +``` +packages/b2c-grafana-datasource/ +├── go.mod # Go module definition +├── go.sum # Dependency checksums +├── package.json # npm scripts +├── Makefile # Build automation +├── docker-compose.yml # Demo environment +│ +├── README.md # Overview +├── docs/ +│ ├── quickstart.md # 5-minute setup +│ ├── configuration.md # Datasource settings +│ ├── query-editor.md # Query builder guide +│ ├── api-reference.md # CallResource endpoints +│ └── architecture.md # This file +│ +├── pkg/ +│ ├── main.go # Metrics plugin entry +│ └── plugin/ +│ └── datasource.go # Metrics core logic +│ +├── pkg/cip/ +│ └── plugin/ +│ ├── main.go # CIP plugin entry +│ ├── datasource.go # CIP core logic +│ ├── driver.go # sqlds driver +│ └── macros.go # SQL macro expansion +│ +├── src/ +│ ├── module.ts # Metrics frontend entry +│ ├── plugin.json # Metrics plugin metadata +│ ├── ConfigEditor.tsx # Metrics config UI +│ ├── QueryEditor.tsx # Metrics query UI +│ ├── datasource.ts # Metrics DataSourceApi +│ └── types.ts # Metrics TypeScript types +│ +├── src/cip/ +│ ├── module.ts # CIP frontend entry +│ ├── plugin.json # CIP plugin metadata +│ ├── ConfigEditor.tsx # CIP config UI +│ ├── QueryEditor.tsx # CIP query UI (SQL editor) +│ ├── VariableQueryEditor.tsx # CIP variable queries +│ ├── datasource.ts # CIP DataSourceApi +│ └── types.ts # CIP TypeScript types +│ +└── dist/ # Build output (gitignored) + ├── gpx_b2c_metrics # Metrics backend binary + ├── gpx_b2c_cip # CIP backend binary + ├── module.js # Metrics frontend bundle + └── cip/module.js # CIP frontend bundle +``` + +## Parity with TypeScript SDK + +The Go SDK ensures exact parity with the TypeScript SDK for: + +1. **Tag Extraction**: 32 golden test cases shared between TS and Go +2. **Window Resolution**: Same retention/default/clamping logic +3. **Tenant ID Normalization**: Identical prefix stripping +4. **Timestamp Handling**: Both normalize API seconds → milliseconds +5. **OAuth Scopes**: Same scope construction + +Any drift is caught by the golden test harness (`operations/metrics/catalog_parity_test.go`). + +## Next Steps + +- **Configuration**: Set up datasources in [Configuration Guide](./configuration.md) +- **Query Editor**: Build queries in [Query Editor Guide](./query-editor.md) +- **API Reference**: Explore CallResource endpoints in [API Reference](./api-reference.md) diff --git a/packages/b2c-grafana-datasource/docs/configuration.md b/packages/b2c-grafana-datasource/docs/configuration.md new file mode 100644 index 000000000..27af14471 --- /dev/null +++ b/packages/b2c-grafana-datasource/docs/configuration.md @@ -0,0 +1,274 @@ +# Configuration Guide + +Complete guide to configuring the B2C Commerce datasources in Grafana. + +## Datasource Types + +The B2C Commerce plugin provides two datasource types: + +1. **Salesforce B2C Commerce Metrics** (`salesforce-b2c-metrics-datasource`) + - Access to the Metrics API (9 endpoint categories) + - Time-series metrics for SCAPI, OCAPI, eCDN, MRT, etc. + - 30-day retention + +2. **Salesforce B2C Commerce Intelligence (CIP)** (`salesforce-b2c-cip-datasource`) + - Raw SQL access to the CIP analytics warehouse + - Calcite SQL dialect with Grafana time macros + - Typically 13-month retention + +Both datasources use OAuth2 client credentials flow and support multi-tenant configurations. + +## Adding a Datasource + +### Via Grafana UI + +1. Navigate to **Configuration → Data sources** (or **Connections → Data sources** in Grafana 10+) +2. Click **Add data source** +3. Search for **Salesforce B2C Commerce Metrics** or **Salesforce B2C Commerce Intelligence (CIP)** +4. Configure settings (see below) +5. Click **Save & Test** + +### Via Provisioning + +For automated deployments, use Grafana's provisioning system. Create a YAML file in `/etc/grafana/provisioning/datasources/`: + +```yaml +apiVersion: 1 +datasources: + - name: B2C Commerce Metrics + type: salesforce-b2c-metrics-datasource + access: proxy + uid: b2c-metrics-prod + jsonData: + shortCode: kv7kzm78 + tenantId: bdpx_prd + accountManagerHost: account.demandware.com + secureJsonData: + clientId: your-client-id + clientSecret: your-client-secret +``` + +## Configuration Fields + +### Metrics Datasource + +#### JSON Data (plaintext, stored in Grafana database) + +- **Short Code** (required): Your B2C instance short code (e.g., `kv7kzm78`) + - Found in Business Manager URL or from Account Manager + - Used to construct API endpoint: `https://{shortCode}.api.commercecloud.salesforce.com` + +- **Tenant ID** (required): Your tenant/realm ID (e.g., `bdpx_prd`) + - Format: `{realm}_{environment}` (e.g., `bdpx_prd`, `bdpx_stg`) + - Or prefixed: `f_ecom_bdpx_prd` (backend normalizes automatically) + - Used for OAuth scoping and organization ID + +- **Account Manager Host** (optional): OAuth token endpoint host + - Default: `account.demandware.com` + - Only change if using a non-standard Account Manager instance + +#### Secure JSON Data (encrypted at rest) + +- **Client ID** (required): OAuth client ID + - Must have `sfcc.metrics` scope + - Obtain from Account Manager + +- **Client Secret** (required): OAuth client secret + - Stored encrypted in Grafana database + - Decrypted only server-side during API calls + +### CIP Datasource + +#### JSON Data + +- **CIP Host** (required): CIP query endpoint host + - Format: `https://cip-{region}.commercecloud.salesforce.com` + - Example: `https://cip-us.commercecloud.salesforce.com` + +- **Tenant ID** (required): Same as Metrics datasource + +- **Account Manager Host** (optional): Same as Metrics datasource + +#### Secure JSON Data + +- **Client ID** (required): OAuth client ID + - Must have appropriate CIP query scope + +- **Client Secret** (required): OAuth client secret + +## OAuth Authentication + +Both datasources use OAuth2 **client credentials flow**: + +1. Backend exchanges client ID + secret for access token +2. Token cached in memory (scope-specific) until expiry +3. Token includes required scopes: + - Metrics: `sfcc.metrics` + `SALESFORCE_COMMERCE_API:{tenantId}` + - CIP: CIP-specific scope + tenant scope +4. Auto-refresh on 401 (token expiry) + +### Token Caching + +- **Cache key**: `host:clientId:method:scopes` (sorted, comma-joined) +- **Lifetime**: Token reused until `expires_in - 60s` margin +- **Memory**: ~2KB per cached token +- **Concurrency**: Single-flight token fetch (prevents duplicate requests) + +### Security Model + +- **Credentials storage**: + - Client ID: Stored plaintext (non-sensitive identifier) + - Client Secret: Encrypted in Grafana database, decrypted server-side only +- **Token storage**: In-memory only (never persisted to disk) +- **Network**: All calls over HTTPS (TLS 1.2+) + +## Multi-tenant Configuration + +### Scenario 1: Same Realm, Multiple Environments + +When staging and production share the same realm and OAuth client: + +**Option A: Single datasource + dashboard variable** + +1. Configure datasource with default tenant (e.g., `bdpx_prd`) +2. Create dashboard variable `$tenant` (type: Text box) +3. Queries can override `tenantId` per panel +4. User enters tenant in variable (e.g., `bdpx_stg`) + +This leverages the Metrics datasource's per-query `tenantId` override feature. + +**Option B: Multiple datasources** + +1. Add one datasource per environment +2. Name them descriptively: `B2C Metrics (Production)`, `B2C Metrics (Staging)` +3. Create dashboard variable of type **Data source**, filtered to `salesforce-b2c-metrics-datasource` +4. Panels reference `${datasource}` variable + +### Scenario 2: Different Realms or Credentials + +When tenants have different shortCodes or OAuth clients: + +1. Add one datasource per tenant (each with its own credentials) +2. Use datasource template variable for dynamic switching +3. Each tenant's credentials stay isolated + +## Health Check + +The **Save & Test** button runs a health check: + +### Metrics Datasource Health Check + +1. Validates required fields (shortCode, tenantId, credentials) +2. Acquires OAuth token +3. Fetches last 5 minutes of "overall" metrics +4. Returns success or error message + +**Success**: "Successfully connected to B2C Commerce Metrics API" + +**Common Errors**: +- **401 Unauthorized**: Invalid client ID or secret +- **403 Forbidden**: Client lacks tenant scope or `sfcc.metrics` scope +- **404 Not Found**: Wrong shortCode or tenant ID +- **Network error**: Firewall, DNS, or connectivity issue + +### CIP Datasource Health Check + +1. Validates configuration +2. Acquires OAuth token +3. Executes `SELECT 1` test query +4. Returns success or error message + +## Advanced Settings + +### Custom API Endpoints (Demo/Testing) + +For local development or custom deployments, you can override API endpoints: + +```yaml +jsonData: + apiUrl: "http://localhost:8080/observability/metrics/v1" + tokenUrl: "http://localhost:8080/dwsso/oauth2/access_token" + shortCode: "demo" + tenantId: "f_ecom_bdpx_prd" +``` + +This is used by the demo Docker Compose setup (connects to mock server). + +### Timeout Configuration + +Currently uses Go's default HTTP client settings: +- **Connection timeout**: 30s +- **Keep-alive**: 90s +- **Idle connections**: 100 per host + +Future versions may expose timeout configuration in the UI. + +## Datasource Permissions + +Grafana's datasource permissions control who can: +- **Query**: Execute queries against the datasource +- **View**: See datasource configuration (except secrets) +- **Edit**: Modify datasource settings +- **Admin**: Delete datasource, manage permissions + +For shared Grafana instances, restrict **Edit** and **Admin** permissions to platform admins. + +## Best Practices + +### Credential Management + +1. **Use dedicated OAuth clients** per Grafana instance (isolates token usage) +2. **Rotate secrets** regularly (update secureJsonData) +3. **Audit access logs** in Account Manager + +### Multi-tenant Strategy + +1. **Same realm**: Use single datasource + dashboard variables +2. **Different realms**: Use separate datasources +3. **Credential isolation**: Never share OAuth clients across security boundaries + +### CIP Read-only Access + +**Important**: CIP datasource executes arbitrary SQL entered in the query editor. Grafana editors are trusted users, but as a defense-in-depth measure: + +1. Use a **read-only database account** for CIP datasource credentials +2. Grant `SELECT` privileges only (no `INSERT`, `UPDATE`, `DELETE`) +3. Limit to necessary tables/schemas via database permissions + +The CIP backend attempts to validate queries start with `SELECT`, but SQL injection via template variables is possible. Read-only credentials limit blast radius. + +### Network Security + +- **Firewall**: Allow Grafana server outbound HTTPS (443) to: + - `account.demandware.com` (OAuth) + - `{shortCode}.api.commercecloud.salesforce.com` (Metrics) + - `cip-{region}.commercecloud.salesforce.com` (CIP) +- **TLS**: Enforce TLS 1.2+ (Go default) +- **Certificate validation**: Default enabled (disable only for testing with self-signed certs) + +## Troubleshooting Configuration + +### "Failed to parse settings" + +- **Cause**: Invalid JSON in jsonData or secureJsonData +- **Fix**: Verify JSON syntax in provisioning YAML, or re-enter via UI + +### "Missing required field: shortCode" + +- **Cause**: Configuration incomplete +- **Fix**: Fill in all required fields before saving + +### Token acquisition hangs + +- **Cause**: Network connectivity to Account Manager blocked +- **Fix**: Check firewall rules, DNS resolution for `account.demandware.com` + +### "Tenant not found" + +- **Cause**: Tenant ID doesn't match any authorized tenant for this client +- **Fix**: Verify tenant ID format and client authorization in Account Manager + +## Next Steps + +- **Query Editor**: Learn how to build queries in [Query Editor Guide](./query-editor.md) +- **Architecture**: Understand token caching and request flow in [Architecture Guide](./architecture.md) diff --git a/packages/b2c-grafana-datasource/docs/query-editor.md b/packages/b2c-grafana-datasource/docs/query-editor.md new file mode 100644 index 000000000..9cb81b109 --- /dev/null +++ b/packages/b2c-grafana-datasource/docs/query-editor.md @@ -0,0 +1,481 @@ +# Query Editor Guide + +Complete guide to building queries with the B2C Commerce datasources. + +## Metrics Datasource Query Editor + +The Metrics datasource uses a tiered filtering interface to build queries against the Metrics API. + +### Query Structure + +Queries are built in this order: + +1. **Category** (required): Which Metrics API endpoint to call +2. **Metrics** (multi-select): Which specific metrics to display +3. **Server Filters** (category-specific): Filter at API level (reduces data transfer) +4. **Label Filters** (optional): Post-fetch filtering on enriched dimensions +5. **Group By** (optional): Controls series naming in legends +6. **Format** (dropdown): Time series (0) or Table (1) + +### 1. Category Selection + +Choose from 9 metric categories: + +| Category | Description | Typical Metrics | +|---|---|---| +| **overall** | High-level system metrics | Total calls, errors, latency | +| **sales** | Revenue and order metrics | Orders, AOV, revenue | +| **ecdn** | Edge CDN performance | Cache hits/misses, bandwidth, status codes | +| **third-party** | External service calls | Latency, errors, timeouts by service | +| **scapi** | Shopper API performance | Calls, latency, cache by API family | +| **scapi-hooks** | Hook execution metrics | Execution time, errors by hook | +| **mrt** | Managed Runtime performance | Requests, errors, p95 latency | +| **controller** | SFRA controller metrics | Calls, latency by pipeline/controller | +| **ocapi** | Open Commerce API | Calls, errors by category/endpoint | + +### 2. Metrics Multi-select + +After selecting a category, the available metrics appear. Select one or more metrics to display: + +- **Single metric**: Clean chart with one unit (e.g., "totalCalls" in requests/sec) +- **Multiple metrics**: Useful when metrics share units (e.g., "cacheHits" + "cacheMisses") +- **Mixed units**: Grafana handles multiple Y-axes, but legends can be noisy + +**Tip**: Start with a single metric per panel for clarity. + +### 3. Server Filters (Category-specific) + +Filters reduce data at the API level before it reaches Grafana. Available filters depend on category: + +#### SCAPI Category + +- **API Family**: Filter to specific API family + - Examples: `product`, `checkout`, `customer`, `search` + - Leave empty to see all families + +- **API Name**: Filter to specific API + - Examples: `shopper-products`, `shopper-baskets`, `shopper-customers` + - Requires API Family when used + +- **API Version**: Filter to specific version (optional) + - Example: `v1`, `v2` + +#### OCAPI Category + +- **OCAPI Category**: Filter to OCAPI category + - Examples: `shop`, `data` + +- **OCAPI API**: Filter to specific endpoint + - Depends on category selected + +#### Third-party Category + +- **Service ID**: Filter to specific external service identifier + - Examples: payment processor IDs, shipping service IDs + +#### Other Categories + +Most other categories have no server filters (return aggregated metrics). + +### 4. Label Filters (Post-fetch) + +After data is fetched, you can filter on any enriched label. Available labels vary by category: + +**Common Labels**: +- `realm`: Tenant realm (e.g., `bdpx`) +- `environment`: Environment (e.g., `prd`, `stg`) +- `metricId`: The metric identifier + +**Category-specific Labels**: +- **SCAPI**: `apiFamily`, `apiName`, `apiVersion`, `cacheStatus` (HIT/MISS), `statusClass` (2xx/4xx/5xx) +- **eCDN**: `host` (PoP location), `cacheStatus`, `statusClass` +- **OCAPI**: `ocapiCategory`, `ocapiApi`, `statusClass` +- **Controller**: `controller` (pipeline name) +- **Third-party**: `host` (service identifier), `exceptionType` + +**Filter Syntax**: +- **Equals**: `apiFamily=product` +- **Not equals**: `apiFamily!=product` +- **Regex match**: `apiFamily=~product|checkout` +- **Multiple filters**: Add multiple rows (AND logic) + +### 5. Group By + +Controls how series are named and grouped in legends. Select one or more labels to group by: + +- **No group-by**: Single series per metric (aggregated) +- **Single label**: One series per label value (e.g., group by `apiFamily` → separate lines for `product`, `checkout`, etc.) +- **Multiple labels**: One series per combination (e.g., group by `apiFamily,cacheStatus` → `product·HIT`, `product·MISS`, etc.) + +**Series Naming**: +- Grouped values joined with `·` (e.g., `product · HIT`) +- If collision, distinguishing label auto-appended (e.g., `product (HIT)`, `product (MISS)`) +- Series with no data points in window are hidden + +**Custom Legends**: +Override legend via **Panel → Standard options → Display name** with template: + +``` +${__field.labels.apiFamily} - ${__field.labels.cacheStatus} +``` + +Available keys: `metricId`, `apiFamily`, `apiName`, `statusClass`, `cacheStatus`, `host`, `ocapiCategory`, `controller`, `exceptionType`, `aggregation`, `realm`, `environment`. + +### 6. Format + +- **Time series (0)**: Standard time-series visualization (default) +- **Table (1)**: Tabular format showing all label columns + +Most panels use Time series format. + +### Example Queries + +**Monitor Product API Cache Performance**: +``` +Category: scapi +Metrics: cacheHitRate +Server Filters: + - apiFamily: product + - apiName: shopper-products +Group By: cacheStatus +``` + +**Track eCDN Error Rate Across All PoPs**: +``` +Category: ecdn +Metrics: errorRate +Label Filters: statusClass=~5xx +Group By: host +``` + +**OCAPI Shop Endpoint Latency**: +``` +Category: ocapi +Metrics: p95Latency +Server Filters: + - ocapiCategory: shop +Group By: ocapiApi +``` + +## CIP Datasource Query Editor + +The CIP datasource provides raw SQL access to the analytics warehouse using Calcite SQL dialect. + +### Query Editor Interface + +- **SQL Editor**: CodeMirror-based SQL editor with syntax highlighting +- **Schema Browser**: Browse available tables and columns +- **Format**: Time series vs Table +- **Time Column**: Which column contains timestamps (for time series) +- **Metric Column**: Which column contains values (for time series) +- **Fill Mode**: How to handle missing data points + +### Writing SQL Queries + +#### Basic Query + +```sql +SELECT + submit_date, + COUNT(*) as order_count +FROM orders +WHERE $__timeFilter(submit_date) +GROUP BY submit_date +ORDER BY submit_date +``` + +#### Calcite SQL Dialect + +CIP uses Apache Calcite SQL (similar to PostgreSQL with differences): + +**Supported**: +- Standard SELECT, FROM, WHERE, GROUP BY, ORDER BY, LIMIT +- JOINs (INNER, LEFT, RIGHT, FULL) +- Subqueries and CTEs (WITH) +- Aggregations: COUNT, SUM, AVG, MIN, MAX +- String functions: UPPER, LOWER, SUBSTRING, CONCAT +- Date functions: EXTRACT, FLOOR, CEIL + +**Not Supported** (compared to PostgreSQL): +- `::type` casting (use `CAST(x AS type)`) +- `INTERVAL '1 day'` (use integer arithmetic) +- Some PostgreSQL-specific functions + +### Grafana Time Macros + +The backend replaces macros with Calcite SQL before execution: + +#### `$__timeFilter(column)` + +Filters rows to Grafana's selected time range. + +**Example**: +```sql +WHERE $__timeFilter(submit_date) +``` + +**Expands to**: +```sql +WHERE submit_date >= TIMESTAMP '2026-07-13 00:00:00' + AND submit_date < TIMESTAMP '2026-07-14 00:00:00' +``` + +#### `$__timeGroup(column, interval)` + +Groups timestamps into time buckets. + +**Example**: +```sql +GROUP BY $__timeGroup(submit_date, 1h) +``` + +**Expands to**: +```sql +GROUP BY FLOOR(submit_date TO HOUR) +``` + +**Supported intervals**: `5m`, `15m`, `30m`, `1h`, `6h`, `12h`, `1d`, `1w` + +#### `$__timeGroupAlias(column, interval)` + +Same as `$__timeGroup` but adds `AS "time"` alias (required for time series visualization). + +**Example**: +```sql +SELECT + $__timeGroupAlias(submit_date, 1h), + SUM(revenue) as total_revenue +FROM orders +WHERE $__timeFilter(submit_date) +GROUP BY $__timeGroup(submit_date, 1h) +ORDER BY 1 +``` + +**Expands to**: +```sql +SELECT + FLOOR(submit_date TO HOUR) AS "time", + SUM(revenue) as total_revenue +FROM orders +WHERE submit_date >= ... AND submit_date < ... +GROUP BY FLOOR(submit_date TO HOUR) +ORDER BY 1 +``` + +#### `$__interval` + +Grafana's auto-calculated interval (e.g., `5m`, `1h`). Use with `$__timeGroup`: + +```sql +GROUP BY $__timeGroup(submit_date, $__interval) +``` + +This adapts bucket size based on dashboard time range (wider range → bigger buckets). + +### Schema Browser + +Click **Schema Browser** to explore available tables: + +**Structure**: +``` +CIP Warehouse +├── orders +│ ├── order_id (STRING) +│ ├── submit_date (TIMESTAMP) +│ ├── revenue (DECIMAL) +│ └── ... +├── products +│ ├── product_id (STRING) +│ ├── name (STRING) +│ └── ... +└── ... +``` + +**Usage**: +1. Browse tables and columns +2. Click table name to insert into query +3. Click column to append to SELECT clause + +**Note**: Schema is live from CIP metadata (reflects current warehouse structure). + +### Template Variables + +Use Grafana template variables in SQL queries: + +**Variable Setup** (Dashboard Settings → Variables): +``` +Name: tenant +Type: Text box +Default: bdpx_prd +``` + +**Query with Variable**: +```sql +SELECT * FROM orders +WHERE tenant_id = '${tenant}' +``` + +**Variable Query** (populate from data): + +In Variable settings, set **Query type** to **Query** and enter SQL: + +```sql +SELECT DISTINCT site_id FROM orders ORDER BY site_id +``` + +This populates a dropdown with site IDs from the warehouse. + +**Note**: Use read-only credentials for CIP datasource to prevent SQL injection attacks via template variables. + +### Time Series vs Table Format + +#### Time Series Format + +Requirements: +- Must have a column named `time` (use `$__timeGroupAlias` macro) +- Must have at least one numeric value column +- Rows must be ordered by `time` ascending + +**Example**: +```sql +SELECT + $__timeGroupAlias(submit_date, $__interval), + COUNT(*) as orders +FROM orders +WHERE $__timeFilter(submit_date) +GROUP BY $__timeGroup(submit_date, $__interval) +ORDER BY 1 +``` + +Grafana renders this as a time-series line chart. + +#### Table Format + +No requirements — any SELECT result displays as a table. + +**Example**: +```sql +SELECT + site_id, + COUNT(*) as order_count, + SUM(revenue) as total_revenue +FROM orders +WHERE $__timeFilter(submit_date) +GROUP BY site_id +ORDER BY total_revenue DESC +LIMIT 10 +``` + +Useful for Top-N queries, data exploration, or alerting on thresholds. + +### Fill Mode + +Controls how missing data points are handled in time series: + +- **null**: Leave gaps (default, shows breaks in data) +- **previous**: Forward-fill (repeat last value) +- **0**: Fill with zero (useful for count metrics) + +Set via **Transform → Replace missing values** in panel settings. + +### Example CIP Queries + +**Orders Over Time**: +```sql +SELECT + $__timeGroupAlias(submit_date, 1h), + COUNT(*) as orders, + SUM(revenue) as revenue +FROM orders +WHERE $__timeFilter(submit_date) + AND site_id = 'RefArch' +GROUP BY $__timeGroup(submit_date, 1h) +ORDER BY 1 +``` + +**Top Products by Revenue**: +```sql +SELECT + p.name, + SUM(li.price * li.quantity) as revenue +FROM order_line_items li + JOIN products p ON li.product_id = p.product_id +WHERE $__timeFilter(li.submit_date) +GROUP BY p.name +ORDER BY revenue DESC +LIMIT 10 +``` + +**Conversion Funnel**: +```sql +SELECT + $__timeGroupAlias(event_date, 1d) as time, + SUM(CASE WHEN event_type = 'page_view' THEN 1 ELSE 0 END) as views, + SUM(CASE WHEN event_type = 'add_to_cart' THEN 1 ELSE 0 END) as adds, + SUM(CASE WHEN event_type = 'checkout' THEN 1 ELSE 0 END) as checkouts +FROM events +WHERE $__timeFilter(event_date) +GROUP BY $__timeGroup(event_date, 1d) +ORDER BY 1 +``` + +## Query Performance Tips + +### Metrics Datasource + +1. **Use server filters**: Reduce data at API level (apiFamily, ocapiCategory, etc.) +2. **Time range**: Metrics API retention is 30 days; queries beyond that are clamped +3. **Single metric**: Start with one metric per panel for clarity +4. **Group-by sparingly**: Too many grouped series can overwhelm visualizations + +### CIP Datasource + +1. **Always use `$__timeFilter`**: Don't query entire warehouse history +2. **Limit rows**: Add `LIMIT` for table queries (default max: 1000 rows) +3. **Index-aware**: CIP tables are partitioned by time; time filters are essential +4. **Aggregate**: Use `GROUP BY` to reduce cardinality +5. **Avoid SELECT ***: Name specific columns (better performance + clarity) +6. **Use variable queries**: Populate dropdowns with `DISTINCT` queries + +## Troubleshooting Queries + +### Metrics Datasource + +**"No data" with valid query**: +- Check category has data for your tenant (try "overall" first) +- Expand time range (mock generates 1 point / 5 min) +- Remove filters (may be too restrictive) + +**"Time range was clamped"**: +- Normal for queries >30 days (Metrics API retention limit) +- Adjust dashboard time range to last 30 days + +**Wrong units in legend**: +- Mixed metric units → create separate panels +- Or use Grafana's multi-axis feature + +### CIP Datasource + +**"Syntax error near..."**: +- Verify Calcite SQL syntax (not PostgreSQL) +- Use `CAST(x AS type)`, not `x::type` +- Check macro expansion (preview in query inspector) + +**"Column 'time' not found"**: +- For time series, must use `$__timeGroupAlias` (creates `time` alias) +- Or manually: `some_column AS "time"` + +**Query timeout**: +- Add `$__timeFilter` (don't scan whole warehouse) +- Reduce time range +- Add `LIMIT` clause + +**Empty result but data exists**: +- Check template variable values (wrong tenant/site?) +- Verify time range covers data (CIP retention varies by table) + +## Next Steps + +- **Configuration**: Set up datasources in [Configuration Guide](./configuration.md) +- **API Reference**: Understand CallResource endpoints in [API Reference](./api-reference.md) +- **Architecture**: Learn how queries are processed in [Architecture Guide](./architecture.md) diff --git a/packages/b2c-grafana-datasource/docs/quickstart.md b/packages/b2c-grafana-datasource/docs/quickstart.md new file mode 100644 index 000000000..2b2f32664 --- /dev/null +++ b/packages/b2c-grafana-datasource/docs/quickstart.md @@ -0,0 +1,291 @@ +# Quick Start Guide + +Get started with the B2C Commerce Grafana datasources in 5 minutes using Docker. + +## Prerequisites + +- Docker 20+ with Compose v2+ +- ~5 minutes for initial build + +## Demo Mode with Mock Data + +Demo mode runs the plugin with a local mock Metrics API for testing — no B2C credentials required. + +### Launch Demo Environment + +From the plugin directory (`packages/b2c-grafana-datasource`): + +```bash +# Using make +make demo + +# Or using docker compose directly +docker compose up --build +``` + +This starts two services: +- **mock-metrics** (port 8080): Mock OAuth + Metrics API with synthetic data +- **grafana** (port 3000): Grafana with both B2C datasource plugins pre-configured + +### Access Grafana + +1. Open http://localhost:3000 in your browser +2. Grafana is pre-configured for anonymous admin access (no login required) +3. Navigate to **Dashboards → B2C Commerce Metrics — Demo** + +The demo dashboard includes panels for: +- Overall total calls +- SCAPI request latency by API family +- SCAPI cache hit rate +- OCAPI calls by category +- eCDN responses by status class +- Third-party service latency by host +- Controller latency by pipeline + +### Mock Data Behavior + +The provisioned datasource `B2C Commerce Metrics (Demo)` connects to the local mock server: + +```yaml +jsonData: + apiUrl: "http://mock-metrics:8080/observability/metrics/v1" + tokenUrl: "http://mock-metrics:8080/dwsso/oauth2/access_token" + shortCode: "demo" + tenantId: "f_ecom_bdpx_prd" + clientId: "demo" +secureJsonData: + clientSecret: "demo" +``` + +The mock server: +- Accepts any credentials (no validation) +- Returns synthetic time-series data spanning the requested time window +- Generates realistic values per category (~1 point / 5 minutes) +- Uses packed series IDs for tag enrichment (e.g., "bdpx.product HIT", "2xx bdpx.host") + +### Stop Demo + +```bash +make down +# or +docker compose down +``` + +## Real Mode with Live B2C Tenant + +Connect to a real B2C Commerce tenant using credentials from the **b2c CLI**. + +### Prerequisites + +- b2c CLI installed and configured (has credentials in dw.json + keychain) +- B2C Commerce tenant with Metrics API access (CLOSED BETA) +- Client credentials with `sfcc.metrics` scope + +### Launch with Real Credentials + +The `real` make target resolves credentials from the b2c CLI and injects them into Grafana at boot. No secrets are written to disk or committed. + +```bash +# Uses the CLI instance named "bdpx-prd" by default +make real + +# Or specify any configured CLI instance +make real INSTANCE=zzpq-019 +``` + +Under the hood this: +1. Runs `b2c setup inspect -i --json --unmask` to get credentials +2. Exports `clientId`, `clientSecret`, `shortCode`, `tenantId` as `B2C_*` env vars +3. Boots `docker-compose.yml` + `docker-compose.real.yml` (swaps provisioning) +4. Backend derives real `https://{shortCode}.api.commercecloud.salesforce.com` endpoints + +The provisioned datasource connects to the live Metrics API. All other steps are identical to demo mode. + +## Multi-tenant Dashboards + +Two ways to switch tenants without editing the datasource: + +### Same-realm Multi-tenant (Recommended) + +Use the **`$tenant` variable** built into the demo dashboard. When multiple tenants share the same realm and OAuth client (common for staging/production on the same realm): + +1. In the dashboard, find the **Tenant** textbox variable at the top +2. Enter a tenant ID (e.g., `bdpx_stg` for staging, `bdpx_prd` for production) +3. All panels query that tenant using per-query `tenantId` override +4. Leave blank to use the datasource's configured tenant + +This works when the datasource's OAuth client is authorized for all tenants (same realm/shortCode). + +### Cross-realm Multi-tenant + +For tenants with different credentials or shortCodes, create one datasource per tenant: + +1. Add multiple datasources (one per tenant) with different credentials +2. Create a dashboard variable of type **Data source** +3. Filter to `salesforce-b2c-metrics-datasource` type +4. Panels reference `${datasource}` variable +5. Each tenant's credentials stay isolated + +This is native Grafana — no plugin code needed. + +## Your First Query + +### Create a Visualization + +1. Click **Create → Dashboard** +2. Click **Add visualization** +3. Select **Salesforce B2C Commerce Metrics** as datasource + +### Configure Query + +Start with a simple query to verify connectivity: + +**Category**: `overall` (shows high-level system metrics) + +Click **Run Query** — you should see time-series data appear. + +### Example Queries + +**Monitor SCAPI Performance**: +- Category: `scapi` +- API Family: `product` +- API Name: `shopper-products` + +Shows request rates, latency, error rates for Product API. + +**Track eCDN Edge Performance**: +- Category: `ecdn` + +Shows edge CDN cache hits/misses, bandwidth, errors across all PoPs. + +**OCAPI Usage**: +- Category: `ocapi` +- OCAPI Category: `shop` + +Shows OCAPI shop endpoint performance. + +## Query Editor Basics + +### Metrics Datasource + +The query editor has three tiers of filtering: + +1. **Category** (required): Select metric category (overall, scapi, ecdn, etc.) +2. **Metrics** (multi-select): Choose specific metrics to display +3. **Filters** (category-specific): Narrow by API family, host, etc. +4. **Label Filters** (optional): Post-fetch filtering on any enriched tag +5. **Group By** (optional): Controls series naming in legends + +### CIP Datasource + +The CIP datasource lets you query the analytics warehouse with raw SQL: + +1. Write Calcite SQL in the editor +2. Use time macros: `$__timeFilter(column)`, `$__timeGroup(column, $__interval)`, `$__timeGroupAlias(column, $__interval)` +3. Browse available tables via **Schema Browser** +4. Use template variables for dynamic queries + +See [Query Editor Guide](./query-editor.md) for details. + +## Time Range Handling + +- **Metrics retention**: 30 days maximum +- **Default window**: 24 hours (when not specified) +- **Clamping**: Queries beyond 30 days are automatically clamped with a warning notice +- **CIP retention**: Varies by table (typically 13 months for fact tables) + +## Troubleshooting + +### Plugin Not Loading + +**Symptom**: Grafana shows "Plugin not found" or "Failed to load plugin" + +**Solutions**: + +1. **Unsigned plugin not allowed**: + - Verify `GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS` includes plugin IDs: + - `salesforce-b2c-metrics-datasource` + - `salesforce-b2c-cip-datasource` + - Check Grafana logs: `make logs` + +2. **Backend binary architecture mismatch**: + - Grafana looks for `gpx_b2c_metrics__` matching the container + - Dockerfile builds for both `linux_amd64` and `linux_arm64` + - If different architecture, rebuild with `make build-backend` + +3. **Missing files in plugin directory**: + - Verify each plugin directory contains: + - `plugin.json` + - `module.js` + - `img/logo.svg` + - Backend binary (`gpx_b2c_metrics_linux_amd64` or `gpx_b2c_cip_linux_amd64`) + +### Backend Logs + +To see backend plugin logs: + +```bash +# Follow all logs (includes backend) +make logs + +# Or filter for plugin logs +docker compose logs -f grafana | grep gpx_b2c +``` + +### Mock Metrics Service Not Responding + +**Symptom**: "Failed to connect to Metrics API" in health check + +**Solutions**: + +1. Check mock-metrics is running: + ```bash + docker compose ps mock-metrics + ``` + +2. Verify network connectivity: + ```bash + docker compose exec grafana wget -O- http://mock-metrics:8080/dwsso/oauth2/access_token + ``` + +3. Check mock-metrics logs: + ```bash + docker compose logs mock-metrics + ``` + +### Dashboard Shows No Data + +**Possible causes**: + +1. **Time range too narrow**: Mock generates ~1 point / 5 minutes. Expand to 6h or more. +2. **Wrong datasource UID**: Verify panels reference correct datasource UID +3. **Backend not fetching data**: Test datasource via **Connections → Data sources → [datasource] → Save & Test** + +### Health Check Fails: "401 Unauthorized" + +**Cause**: Invalid client credentials + +**Fix**: +1. Verify Client ID and Secret are correct +2. Ensure client has `sfcc.metrics` scope +3. Check tenant ID format (should be `bdpx_prd`, not `f_ecom_bdpx_prd`) + +### Health Check Fails: "403 Forbidden" + +**Cause**: Client lacks tenant scope + +**Fix**: +1. Verify tenant ID matches your B2C instance +2. Ensure client has access to the specified tenant +3. Check short code is correct for the tenant + +### "Time range was clamped" Warning + +**This is normal**: Metrics API retains 30 days. Queries beyond that are automatically adjusted. The warning just informs you the range was changed. + +## Next Steps + +- **Configuration**: Learn about datasource settings in [Configuration Guide](./configuration.md) +- **Query Editor**: Master the query builders in [Query Editor Guide](./query-editor.md) +- **Architecture**: Understand how it works in [Architecture Guide](./architecture.md) +- **API Reference**: CallResource endpoints in [API Reference](./api-reference.md) diff --git a/packages/b2c-grafana-datasource/go.mod b/packages/b2c-grafana-datasource/go.mod new file mode 100644 index 000000000..d29b36e7d --- /dev/null +++ b/packages/b2c-grafana-datasource/go.mod @@ -0,0 +1,124 @@ +module github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-grafana-datasource + +go 1.26 + +require ( + github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go v0.0.0 + github.com/grafana/grafana-plugin-sdk-go v0.278.0 + github.com/grafana/sqlds/v4 v4.2.7 + github.com/magefile/mage v1.15.0 +) + +require ( + github.com/BurntSushi/toml v1.4.0 // indirect + github.com/apache/arrow-go/v18 v18.3.0 // indirect + github.com/apache/calcite-avatica-go/v5 v5.4.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v5 v5.0.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cheekybits/genny v1.0.0 // indirect + github.com/chromedp/cdproto v0.0.0-20250429231605-6ed5b53462d4 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/elazarl/goproxy v1.7.2 // indirect + github.com/fatih/color v1.15.0 // indirect + github.com/getkin/kin-openapi v0.132.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/gogo/googleapis v1.4.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/flatbuffers v25.2.10+incompatible // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/grafana/dataplane/sdata v0.0.9 // indirect + github.com/grafana/otel-profiling-go v0.5.1 // indirect + github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-plugin v1.6.3 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/hashicorp/yamux v0.1.1 // indirect + github.com/icholy/digest v1.1.0 // indirect + github.com/jaegertracing/jaeger-idl v0.5.0 // indirect + github.com/jcmturner/aescts/v2 v2.0.0 // indirect + github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect + github.com/jcmturner/gofork v1.7.6 // indirect + github.com/jcmturner/goidentity/v6 v6.0.1 // indirect + github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect + github.com/jcmturner/rpc/v2 v2.0.3 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattetti/filebuffer v1.0.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mithrandie/csvq v1.18.1 // indirect + github.com/mithrandie/csvq-driver v1.7.0 // indirect + github.com/mithrandie/go-file/v2 v2.1.0 // indirect + github.com/mithrandie/go-text v1.6.0 // indirect + github.com/mithrandie/ternary v1.1.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect + github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect + github.com/oklog/run v1.1.0 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.64.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 // indirect + github.com/unknwon/com v1.0.1 // indirect + github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3 // indirect + github.com/urfave/cli v1.22.16 // indirect + github.com/zeebo/xxh3 v1.0.2 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.61.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.36.0 // indirect + go.opentelemetry.io/contrib/samplers/jaegerremote v0.30.0 // indirect + go.opentelemetry.io/otel v1.36.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect + go.opentelemetry.io/otel/metric v1.36.0 // indirect + go.opentelemetry.io/otel/sdk v1.36.0 // indirect + go.opentelemetry.io/otel/trace v1.36.0 // indirect + go.opentelemetry.io/proto/otlp v1.6.0 // indirect + golang.org/x/crypto v0.39.0 // indirect + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.41.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/term v0.32.0 // indirect + golang.org/x/text v0.26.0 // indirect + golang.org/x/tools v0.33.0 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect + google.golang.org/grpc v1.73.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +// Local monorepo dependency +replace github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go => ../b2c-tooling-sdk-go diff --git a/packages/b2c-grafana-datasource/go.sum b/packages/b2c-grafana-datasource/go.sum new file mode 100644 index 000000000..1471e2f27 --- /dev/null +++ b/packages/b2c-grafana-datasource/go.sum @@ -0,0 +1,393 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/apache/arrow-go/v18 v18.3.0 h1:Xq4A6dZj9Nu33sqZibzn012LNnewkTUlfKVUFD/RX/I= +github.com/apache/arrow-go/v18 v18.3.0/go.mod h1:eEM1DnUTHhgGAjf/ChvOAQbUQ+EPohtDrArffvUjPg8= +github.com/apache/calcite-avatica-go/v5 v5.4.0 h1:snCrhGlwDgqNA2Rp7RUABjNX2zX+EfLk5K7PSJRPD5w= +github.com/apache/calcite-avatica-go/v5 v5.4.0/go.mod h1:ed2DNx4xLzxrVYbvZU9Nv97LwyO6c0J7oGnOP4HbqZk= +github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE= +github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw= +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/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= +github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= +github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= +github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +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/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= +github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= +github.com/chromedp/cdproto v0.0.0-20250429231605-6ed5b53462d4 h1:UZdrvid2JFwnvPlUSEFlE794XZL4Jmrj8fuxfcLECJE= +github.com/chromedp/cdproto v0.0.0-20250429231605-6ed5b53462d4/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk= +github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= +github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= +github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= +github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6kE/MWfg7s= +github.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU= +github.com/grafana/grafana-plugin-sdk-go v0.278.0 h1:5/rIYparLi02pofdaag8wnjspMMVNCi8cZhC4cdC3Ho= +github.com/grafana/grafana-plugin-sdk-go v0.278.0/go.mod h1:+8NXT/XUJ/89GV6FxGQ366NZ3nU+cAXDMd0OUESF9H4= +github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= +github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= +github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= +github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= +github.com/grafana/sqlds/v4 v4.2.7 h1:sFQhsS7DBakNMdxa++yOfJ9BVvkZwFJ0B95o57K0/XA= +github.com/grafana/sqlds/v4 v4.2.7/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0UUrwg= +github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= +github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4= +github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y= +github.com/jaegertracing/jaeger-idl v0.5.0 h1:zFXR5NL3Utu7MhPg8ZorxtCBjHrL3ReM1VoB65FOFGE= +github.com/jaegertracing/jaeger-idl v0.5.0/go.mod h1:ON90zFo9eoyXrt9F/KN8YeF3zxcnujaisMweFY/rg5k= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= +github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 h1:SwcnSwBR7X/5EHJQlXBockkJVIMRVt5yKaesBPMtyZQ= +github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6/go.mod h1:WrYiIuiXUMIvTDAQw97C+9l0CnBmCcvosPjN3XDqS/o= +github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE= +github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= +github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= +github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM= +github.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mithrandie/csvq v1.18.1 h1:f7NB2scbb7xx2ffPduJ2VtZ85RpWXfvanYskAkGlCBU= +github.com/mithrandie/csvq v1.18.1/go.mod h1:MRJj7AtcXfk7jhNGxLuJGP3LORmh4lpiPWxQ7VyCRn8= +github.com/mithrandie/csvq-driver v1.7.0 h1:ejiavXNWwTPMyr3fJFnhcqd1L1cYudA0foQy9cZrqhw= +github.com/mithrandie/csvq-driver v1.7.0/go.mod h1:HcN3xL9UCJnBYA/AIQOOB/KlyfXAiYr5yxDmiwrGk5o= +github.com/mithrandie/go-file/v2 v2.1.0 h1:XA5Tl+73GXMDvgwSE3Sg0uC5FkLr3hnXs8SpUas0hyg= +github.com/mithrandie/go-file/v2 v2.1.0/go.mod h1:9YtTF3Xo59GqC1Pxw6KyGVcM/qubAMlxVsqI/u9r++c= +github.com/mithrandie/go-text v1.6.0 h1:8gOXTMPbMY8DJbKMTv8kHhADcJlDWXqS/YQH4SyWO6s= +github.com/mithrandie/go-text v1.6.0/go.mod h1:xCgj1xiNbI/d4xA9sLVvXkjh5B2tNx2ZT2/3rpmh8to= +github.com/mithrandie/ternary v1.1.1 h1:k/joD6UGVYxHixYmSR8EGgDFNONBMqyD373xT4QRdC4= +github.com/mithrandie/ternary v1.1.1/go.mod h1:0D9Ba3+09K2TdSZO7/bFCC0GjSXetCvYuYq0u8FY/1g= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +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/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= +github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +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/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY= +github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w= +github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 h1:aVGB3YnaS/JNfOW3tiHIlmNmTDg618va+eT0mVomgyI= +github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8/go.mod h1:fVle4kNr08ydeohzYafr20oZzbAkhQT39gKK/pFQ5M4= +github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs= +github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM= +github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3 h1:4EYQaWAatQokdji3zqZloVIW/Ke1RQjYw2zHULyrHJg= +github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3/go.mod h1:1xEUf2abjfP92w2GZTV+GgaRxXErwRXcClbUwrNJffU= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= +github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.61.0 h1:lREC4C0ilyP4WibDhQ7Gg2ygAQFP8oR07Fst/5cafwI= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.61.0/go.mod h1:HfvuU0kW9HewH14VCOLImqKvUgONodURG7Alj/IrnGI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/contrib/propagators/jaeger v1.36.0 h1:SoCgXYF4ISDtNyfLUzsGDaaudZVTx2yJhOyBO0+/GYk= +go.opentelemetry.io/contrib/propagators/jaeger v1.36.0/go.mod h1:VHu48l0YTRKSObdPQ+Sb8xMZvdnJlN7yhHuHoPgNqHM= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.30.0 h1:bQ1Gvah4Sp8z7epSkgJaNTuZm7sutfA6Fji2/7cKFMc= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.30.0/go.mod h1:9b8Q9rH52NgYH3ShiTFB5wf18Vt3RTH/VMB7LDcC1ug= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI= +go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191020152052-9984515f0562/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +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/fsnotify/fsnotify.v1 v1.4.7 h1:XNNYLJHt73EyYiCZi6+xjupS9CpvmiDgjPTAjrBlQbo= +gopkg.in/fsnotify/fsnotify.v1 v1.4.7/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/packages/b2c-grafana-datasource/package.json b/packages/b2c-grafana-datasource/package.json new file mode 100644 index 000000000..cf14cfac9 --- /dev/null +++ b/packages/b2c-grafana-datasource/package.json @@ -0,0 +1,63 @@ +{ + "name": "b2c-grafana-datasource", + "version": "0.1.0", + "private": true, + "description": "Grafana datasource plugin for Salesforce B2C Commerce Metrics API", + "author": "Salesforce", + "license": "Apache-2.0", + "scripts": { + "build": "webpack --config .config/webpack.config.ts --env production", + "build:backend": "go build -o dist/gpx_b2c_metrics ./pkg && go build -o dist/gpx_b2c_cip ./pkg/cip", + "build:backend:all": "CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o dist/gpx_b2c_metrics_linux_amd64 ./pkg && CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o dist/gpx_b2c_metrics_linux_arm64 ./pkg && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o dist/gpx_b2c_cip_linux_amd64 ./pkg/cip && CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o dist/gpx_b2c_cip_linux_arm64 ./pkg/cip", + "dev": "webpack --config .config/webpack.config.ts --watch", + "typecheck": "tsc --noEmit", + "test:backend": "go test -v ./...", + "fmt": "go fmt ./...", + "vet": "go vet ./...", + "clean": "rm -rf dist", + "demo": "docker compose up --build", + "demo:down": "docker compose down", + "demo:logs": "docker compose logs -f" + }, + "repository": { + "type": "git", + "url": "https://github.com/SalesforceCommerceCloud/b2c-developer-tooling.git", + "directory": "packages/b2c-grafana-datasource" + }, + "keywords": [ + "grafana", + "grafana-plugin", + "datasource", + "salesforce", + "b2c-commerce", + "metrics", + "observability" + ], + "dependencies": { + "@grafana/data": "^10.4.0", + "@grafana/runtime": "^10.4.0", + "@grafana/ui": "^10.4.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@grafana/eslint-config": "^7.0.0", + "@grafana/tsconfig": "^2.0.0", + "@swc/core": "^1.4.0", + "@types/node": "^20.11.0", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@types/webpack": "^5.28.5", + "copy-webpack-plugin": "^12.0.2", + "css-loader": "^6.10.0", + "style-loader": "^3.3.4", + "swc-loader": "^0.2.6", + "ts-node": "^10.9.2", + "typescript": "^5.3.0", + "webpack": "^5.90.0", + "webpack-cli": "^5.1.4" + }, + "engines": { + "node": ">=18" + } +} diff --git a/packages/b2c-grafana-datasource/pkg/cip/main.go b/packages/b2c-grafana-datasource/pkg/cip/main.go new file mode 100644 index 000000000..4db6c345a --- /dev/null +++ b/packages/b2c-grafana-datasource/pkg/cip/main.go @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +package main + +import ( + "os" + + "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" + + cipds "github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-grafana-datasource/pkg/cip/plugin" +) + +func main() { + if err := datasource.Manage( + "salesforce-b2c-cip-datasource", + cipds.NewDatasource, + datasource.ManageOpts{}, + ); err != nil { + log.DefaultLogger.Error(err.Error()) + os.Exit(1) + } +} diff --git a/packages/b2c-grafana-datasource/pkg/cip/plugin/datasource.go b/packages/b2c-grafana-datasource/pkg/cip/plugin/datasource.go new file mode 100644 index 000000000..3b6b98369 --- /dev/null +++ b/packages/b2c-grafana-datasource/pkg/cip/plugin/datasource.go @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +package plugin + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" + "github.com/grafana/sqlds/v4" +) + +// NewDatasource is the datasource.InstanceFactoryFunc registered with the plugin. +// It builds a CIP sqlds.Driver and wraps sqlds.NewDatasource, attaching the CIP-specific +// schema-browser and template-variable resource routes. +func NewDatasource(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + drv, err := newDriver(settings) + if err != nil { + return nil, err + } + + ds := sqlds.NewDatasource(drv) + // Custom resource routes for the query editor's schema browser and template + // variables (sqlds owns /tables|/schemas|/columns completion; ours are additive). + ds.CustomRoutes = map[string]func(http.ResponseWriter, *http.Request){ + "/cip/tables": drv.handleTables, + "/cip/columns": drv.handleColumns, + "/cip/sites": drv.handleSites, + "/cip/variable": drv.handleVariable, + } + + inst, err := ds.NewDatasource(ctx, settings) + if err != nil { + return nil, err + } + return inst, nil +} + +// --- CIP resource handlers (schema browser + template variables) --------------- +// +// These operate on the driver's CIP client directly (metadata queries), independent of +// the sqlds query path. They return JSON for the frontend query editor. + +func (d *cipDriver) handleTables(w http.ResponseWriter, r *http.Request) { + tables, err := d.client.ListTables(r.Context(), r.URL.Query().Get("schema")) + writeJSONOrErr(w, tables, err) +} + +func (d *cipDriver) handleColumns(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + cols, err := d.client.DescribeColumns(r.Context(), q.Get("schema"), q.Get("table")) + writeJSONOrErr(w, cols, err) +} + +func (d *cipDriver) handleSites(w http.ResponseWriter, r *http.Request) { + vals, err := d.variableValues(r.Context(), + "SELECT DISTINCT nsite_id FROM warehouse.ccdw_dim_site WHERE nsite_id IS NOT NULL ORDER BY nsite_id") + writeJSONOrErr(w, vals, err) +} + +func (d *cipDriver) handleVariable(w http.ResponseWriter, r *http.Request) { + sqlStr := r.URL.Query().Get("sql") + if sqlStr == "" { + var body struct { + SQL string `json:"sql"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err == nil { + sqlStr = body.SQL + } + } + if strings.TrimSpace(sqlStr) == "" { + http.Error(w, "sql is required", http.StatusBadRequest) + return + } + vals, err := d.variableValues(r.Context(), sqlStr) + writeJSONOrErr(w, vals, err) +} + +// variableValues runs a variable query and returns the DISTINCT values of its first +// column as strings (for populating a Grafana template-variable dropdown). +func (d *cipDriver) variableValues(ctx context.Context, query string) ([]string, error) { + res, err := d.client.Query(ctx, query) + if err != nil { + return nil, err + } + if len(res.Columns) == 0 { + return []string{}, nil + } + col := res.Columns[0] + seen := map[string]bool{} + out := []string{} + for _, row := range res.Rows { + v := row[col] + if v == nil { + continue + } + s := fmt.Sprintf("%v", v) + if s == "" || seen[s] { + continue + } + seen[s] = true + out = append(out, s) + } + return out, nil +} + +func writeJSONOrErr(w http.ResponseWriter, v any, err error) { + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(v) +} diff --git a/packages/b2c-grafana-datasource/pkg/cip/plugin/driver.go b/packages/b2c-grafana-datasource/pkg/cip/plugin/driver.go new file mode 100644 index 000000000..df9c6acfc --- /dev/null +++ b/packages/b2c-grafana-datasource/pkg/cip/plugin/driver.go @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +// Package plugin implements the Grafana backend for the B2C CIP (Commerce Intelligence +// Platform) data source: a raw Calcite-SQL editor over the CIP analytics warehouse. +// +// It is built on the grafana/sqlds framework (the same framework the built-in Postgres, +// MySQL, and community SQL datasources use), which owns the QueryData/CheckHealth/ +// resource plumbing, row limiting, timeouts, macro interpolation, and frame conversion. +// The CIP-specific pieces are the sqlds.Driver implementation below: connecting via the +// Avatica/protobuf client, the Calcite dialect of the standard time macros, the DECIMAL→ +// float converter, and a response mutator for conditional wide-framing. +package plugin + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "reflect" + "strconv" + "strings" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana-plugin-sdk-go/data/sqlutil" + "github.com/grafana/sqlds/v4" + + "github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go/auth" + "github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go/clients" + "github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go/clients/cip" +) + +// defaultRowLimit caps rows returned per query to protect plugin memory against broad +// analytical SELECTs. Mirrors the bounded behavior of mature SQL datasources. +const defaultRowLimit = int64(100000) + +// defaultQueryTimeout bounds a single CIP query. +const defaultQueryTimeout = 60 * time.Second + +// datasourceConfig is the non-secret jsonData for the CIP datasource. +type datasourceConfig struct { + // Instance is the CIP instance id (e.g. "bdpx_prd"). Falls back to TenantId + // (normalized) when empty so a single config can mirror the Metrics datasource. + Instance string `json:"instance"` + TenantID string `json:"tenantId"` + ClientID string `json:"clientId"` + + AccountManagerHost string `json:"accountManagerHost"` + // Host overrides the CIP Avatica host (optional; for staging analytics). + Host string `json:"cipHost"` +} + +// cipDriver implements sqlds.Driver for the CIP Avatica warehouse. One driver instance +// is created per Grafana datasource instance; it owns a single CIP client (whose *sql.DB +// is sticky-session, MaxOpenConns=1). +type cipDriver struct { + client *cip.Client + instance string +} + +// newDriver builds a cipDriver from Grafana datasource settings. +func newDriver(settings backend.DataSourceInstanceSettings) (*cipDriver, error) { + var cfg datasourceConfig + if err := json.Unmarshal(settings.JSONData, &cfg); err != nil { + return nil, fmt.Errorf("failed to parse datasource settings: %w", err) + } + + instance := cfg.Instance + if instance == "" { + instance = clients.NormalizeTenantID(cfg.TenantID) + } + if instance == "" { + return nil, fmt.Errorf("cip datasource requires 'instance' (or 'tenantId')") + } + + clientID := cfg.ClientID + clientSecret := settings.DecryptedSecureJSONData["clientSecret"] + if clientID == "" || clientSecret == "" { + return nil, fmt.Errorf("missing client credentials (clientId in jsonData, clientSecret in secureJsonData)") + } + + authCfg := auth.OAuthConfig{ClientID: clientID, ClientSecret: clientSecret, AccountManagerHost: cfg.AccountManagerHost} + if authCfg.AccountManagerHost == "" { + authCfg.AccountManagerHost = auth.DefaultAccountManagerHost + } + strat := auth.NewOAuthStrategy(authCfg) + + client, err := cip.NewClient(cip.Config{Instance: instance, Host: cfg.Host}, strat) + if err != nil { + return nil, fmt.Errorf("creating CIP client: %w", err) + } + return &cipDriver{client: client, instance: instance}, nil +} + +// Connect returns the CIP client's *sql.DB. sqlds calls this to obtain the connection; +// the msg (connection args) is unused — CIP has a single connection per datasource. +func (d *cipDriver) Connect(_ context.Context, _ backend.DataSourceInstanceSettings, _ json.RawMessage) (*sql.DB, error) { + return d.client.DB(), nil +} + +// Settings supplies framework behavior: a bounded row limit and per-query timeout. +func (d *cipDriver) Settings(_ context.Context, _ backend.DataSourceInstanceSettings) sqlds.DriverSettings { + return sqlds.DriverSettings{ + Timeout: defaultQueryTimeout, + RowLimit: defaultRowLimit, + // FillMode left nil → sqlds default; CIP results are aggregates, not sparse series. + } +} + +// Macros returns the Calcite dialect of the standard Grafana SQL time macros. These use +// sqlutil's parser (so $__interval, multipliers, and arg parsing are handled correctly — +// unlike the previous hand-rolled regex expander), but emit Calcite-specific SQL: +// TIMESTAMP literals and FLOOR(col TO UNIT) bucketing. +func (d *cipDriver) Macros() sqlds.Macros { + return sqlds.Macros{ + // $__timeFilter(col) → col BETWEEN TIMESTAMP '..' AND TIMESTAMP '..' + "timeFilter": func(q *sqlutil.Query, args []string) (string, error) { + col, err := oneArg(args, "timeFilter") + if err != nil { + return "", err + } + return fmt.Sprintf("%s BETWEEN %s AND %s", col, tsLiteral(q.TimeRange.From), tsLiteral(q.TimeRange.To)), nil + }, + // $__timeFrom() / $__timeTo() → TIMESTAMP '..' + "timeFrom": func(q *sqlutil.Query, _ []string) (string, error) { return tsLiteral(q.TimeRange.From), nil }, + "timeTo": func(q *sqlutil.Query, _ []string) (string, error) { return tsLiteral(q.TimeRange.To), nil }, + // $__timeGroup(col, '1d') → FLOOR(col TO DAY) + "timeGroup": func(_ *sqlutil.Query, args []string) (string, error) { + col, unit, err := colAndUnit(args, "timeGroup") + if err != nil { + return "", err + } + return fmt.Sprintf("FLOOR(%s TO %s)", col, unit), nil + }, + // $__timeGroupAlias(col, '1d') → FLOOR(col TO DAY) AS "time" + "timeGroupAlias": func(_ *sqlutil.Query, args []string) (string, error) { + col, unit, err := colAndUnit(args, "timeGroupAlias") + if err != nil { + return "", err + } + return fmt.Sprintf(`FLOOR(%s TO %s) AS "time"`, col, unit), nil + }, + } +} + +// Converters returns the CIP column converters (DECIMAL→float64; see cipConverters). +func (d *cipDriver) Converters() []sqlutil.Converter { + return cipConverters() +} + +// Note on framing: sqlds handles long→wide reshaping itself based on the query Format +// (FormatOptionTimeSeries → LongToWide when the frame is long; FormatOptionTable → as-is) +// and sets PreferredVisualization. So no ResponseMutator is needed — the DECIMAL +// converter is what makes our aggregate columns numeric so that reshaping works. + +// --- macro helpers ------------------------------------------------------------- + +const cipTimeLayout = "2006-01-02 15:04:05" + +func tsLiteral(t time.Time) string { + return fmt.Sprintf("TIMESTAMP '%s'", t.UTC().Format(cipTimeLayout)) +} + +func oneArg(args []string, macro string) (string, error) { + if len(args) < 1 || strings.TrimSpace(args[0]) == "" { + return "", fmt.Errorf("%s requires a column argument", macro) + } + return strings.TrimSpace(args[0]), nil +} + +func colAndUnit(args []string, macro string) (col, unit string, err error) { + if len(args) < 2 { + return "", "", fmt.Errorf("%s requires (column, interval) arguments", macro) + } + col = strings.TrimSpace(args[0]) + unit = calciteFloorUnit(strings.Trim(strings.TrimSpace(args[1]), "'\"")) + if col == "" { + return "", "", fmt.Errorf("%s: empty column", macro) + } + return col, unit, nil +} + +// calciteFloorUnit maps a Grafana duration to a Calcite FLOOR time unit. Calcite's +// FLOOR(datetime TO unit) has no multiplier concept, so we bucket to the unit implied by +// the duration's magnitude (e.g. 30s→SECOND, 5m→MINUTE, 6h→HOUR, 1d→DAY). This is a +// deliberate coarsening; sub-unit multipliers aren't expressible in Calcite FLOOR. +func calciteFloorUnit(dur string) string { + d, err := time.ParseDuration(strings.NewReplacer("d", "h", "w", "h").Replace(normalizeDur(dur))) + if err != nil || d <= 0 { + return "DAY" + } + switch { + case d < time.Minute: + return "SECOND" + case d < time.Hour: + return "MINUTE" + case d < 24*time.Hour: + return "HOUR" + case d < 7*24*time.Hour: + return "DAY" + case d < 30*24*time.Hour: + return "WEEK" + case d < 365*24*time.Hour: + return "MONTH" + default: + return "YEAR" + } +} + +// normalizeDur converts a Grafana-style duration where d/w/M/y are day/week/month/year +// into an hours-based approximation time.ParseDuration can read (only for magnitude +// bucketing in calciteFloorUnit). e.g. "1d"→"24h", "1w"→"168h", "1M"→"720h", "1y"→"8760h". +func normalizeDur(dur string) string { + dur = strings.TrimSpace(dur) + if dur == "" { + return "24h" + } + suffix := dur[len(dur)-1] + num := dur[:len(dur)-1] + n, err := strconv.Atoi(num) + if err != nil { + return dur // let ParseDuration try (handles s/m/h natively) + } + switch suffix { + case 'd': + return fmt.Sprintf("%dh", n*24) + case 'w': + return fmt.Sprintf("%dh", n*24*7) + case 'M': + return fmt.Sprintf("%dh", n*24*30) + case 'y': + return fmt.Sprintf("%dh", n*24*365) + default: + return dur // s/m/h + } +} + +// cipConverters returns sqlutil converters for CIP/Avatica column types that would +// otherwise scan as strings and break charting. Avatica returns DECIMAL as a Go string +// (e.g. "32324.00"); every revenue/AOV/latency/percentage column in the report catalog +// is DECIMAL, so without this they arrive unplottable AND (being string fields) trip the +// long→wide pivot. Converts DECIMAL → nullable float64. DOUBLE/FLOAT already scan as +// float64, BIGINT as int64, DATE/TIMESTAMP as time.Time. +func cipConverters() []sqlutil.Converter { + decimalToFloat := sqlutil.Converter{ + Name: "CIP DECIMAL to float64", + InputScanType: reflect.TypeOf(sql.NullString{}), + InputTypeName: "DECIMAL", + FrameConverter: sqlutil.FrameConverter{ + FieldType: data.FieldTypeNullableFloat64, + ConverterFunc: func(in interface{}) (interface{}, error) { + v, ok := in.(*sql.NullString) + if !ok || v == nil || !v.Valid { + return (*float64)(nil), nil + } + f, err := strconv.ParseFloat(strings.TrimSpace(v.String), 64) + if err != nil { + // Non-numeric DECIMAL text → null (never fail the frame), but log so + // data-quality issues are visible rather than silently dropped. + log.DefaultLogger.Warn("CIP DECIMAL parse failed", "value", v.String, "error", err) + return (*float64)(nil), nil + } + return &f, nil + }, + }, + } + return []sqlutil.Converter{decimalToFloat} +} diff --git a/packages/b2c-grafana-datasource/pkg/cip/plugin/macros_test.go b/packages/b2c-grafana-datasource/pkg/cip/plugin/macros_test.go new file mode 100644 index 000000000..9a0977a05 --- /dev/null +++ b/packages/b2c-grafana-datasource/pkg/cip/plugin/macros_test.go @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +package plugin + +import ( + "strings" + "testing" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data/sqlutil" +) + +func mkQuery(rawSQL string, interval time.Duration) *sqlutil.Query { + return &sqlutil.Query{ + RawSQL: rawSQL, + Interval: interval, + TimeRange: backend.TimeRange{ + From: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), + To: time.Date(2026, 1, 2, 12, 30, 0, 0, time.UTC), + }, + } +} + +// interpolate runs a rawSQL string through the CIP driver's macros exactly as sqlds would. +func interpolate(t *testing.T, rawSQL string, interval time.Duration) string { + t.Helper() + d := &cipDriver{} + q := mkQuery(rawSQL, interval) + out, err := sqlutil.Interpolate(q, d.Macros()) + if err != nil { + t.Fatalf("Interpolate(%q) error: %v", rawSQL, err) + } + return out +} + +func TestCIPMacros(t *testing.T) { + cases := []struct { + name string + in string + want string // substring that must appear + }{ + {"timeFilter", "WHERE $__timeFilter(submit_date)", "submit_date BETWEEN TIMESTAMP '2026-01-01 00:00:00' AND TIMESTAMP '2026-01-02 12:30:00'"}, + {"timeGroup day", "SELECT $__timeGroup(submit_date, '1d')", "FLOOR(submit_date TO DAY)"}, + {"timeGroup hour", "SELECT $__timeGroup(ts, '1h')", "FLOOR(ts TO HOUR)"}, + {"timeGroup 6h→HOUR (multiplier not lost to MINUTE)", "SELECT $__timeGroup(ts, '6h')", "FLOOR(ts TO HOUR)"}, + {"timeGroup 15m→MINUTE", "SELECT $__timeGroup(ts, '15m')", "FLOOR(ts TO MINUTE)"}, + {"timeGroup month", "SELECT $__timeGroup(ts, '1M')", "FLOOR(ts TO MONTH)"}, + {"timeGroupAlias", "SELECT $__timeGroupAlias(submit_date, '1d')", `FLOOR(submit_date TO DAY) AS "time"`}, + {"timeFrom", "x > $__timeFrom()", "TIMESTAMP '2026-01-01 00:00:00'"}, + {"timeTo", "x < $__timeTo()", "TIMESTAMP '2026-01-02 12:30:00'"}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got := interpolate(t, c.in, time.Minute) + if !strings.Contains(got, c.want) { + t.Errorf("interpolate(%q)\n got: %q\n want substring: %q", c.in, got, c.want) + } + }) + } +} + +// TestCIPMacrosInterval verifies the $__interval macro (from sqlutil's default set, +// merged by sqlds) resolves — the previous hand-rolled expander left it unexpanded, +// which broke the shipped default query. +func TestCIPMacrosInterval(t *testing.T) { + got := interpolate(t, "SELECT $__timeGroupAlias(submit_date, $__interval)", 5*time.Minute) + if strings.Contains(got, "$__interval") { + t.Errorf("$__interval left unexpanded: %q", got) + } + if !strings.Contains(got, "FLOOR(submit_date TO") { + t.Errorf("timeGroupAlias with $__interval did not expand to FLOOR: %q", got) + } +} + +func TestCalciteFloorUnit(t *testing.T) { + for dur, want := range map[string]string{ + "30s": "SECOND", "5m": "MINUTE", "15m": "MINUTE", "1h": "HOUR", "6h": "HOUR", + "1d": "DAY", "1w": "WEEK", "1M": "MONTH", "1y": "YEAR", "": "DAY", + } { + if got := calciteFloorUnit(dur); got != want { + t.Errorf("calciteFloorUnit(%q)=%q want %q", dur, got, want) + } + } +} diff --git a/packages/b2c-grafana-datasource/pkg/main.go b/packages/b2c-grafana-datasource/pkg/main.go new file mode 100644 index 000000000..4c690f2fc --- /dev/null +++ b/packages/b2c-grafana-datasource/pkg/main.go @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +package main + +import ( + "os" + + "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" + + "github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-grafana-datasource/pkg/plugin" +) + +func main() { + if err := datasource.Manage( + "salesforce-b2c-metrics-datasource", + plugin.NewDatasource, + datasource.ManageOpts{}, + ); err != nil { + log.DefaultLogger.Error(err.Error()) + os.Exit(1) + } +} diff --git a/packages/b2c-grafana-datasource/pkg/plugin/datasource.go b/packages/b2c-grafana-datasource/pkg/plugin/datasource.go new file mode 100644 index 000000000..40f7dcc3a --- /dev/null +++ b/packages/b2c-grafana-datasource/pkg/plugin/datasource.go @@ -0,0 +1,840 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +package plugin + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "sort" + "strings" + "sync" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/grafana/grafana-plugin-sdk-go/data" + + "github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go/auth" + "github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go/clients/metrics" + metricsops "github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go/operations/metrics" +) + +// B2CMetricsDatasource implements the Grafana datasource interface. +type B2CMetricsDatasource struct { + client *metrics.Client // default client (datasource-configured tenant/shortCode) + auth *auth.OAuthStrategy // retained to build per-query clients for tenant/shortCode overrides + config DatasourceConfig + + mu sync.Mutex + clientCache map[string]*metrics.Client // keyed by tenantId for per-query tenant overrides +} + +// clientFor returns a metrics client for the given tenantId, falling back to the +// datasource-configured tenant when empty. Tenants on the same realm share the +// shortCode and OAuth client (the datasource's), so only the tenant — which drives +// the org path and the per-tenant OAuth scope — varies. Clients are cached by +// tenantId so switching between (e.g.) prd and stg doesn't rebuild auth each query. +func (d *B2CMetricsDatasource) clientFor(tenantID string) *metrics.Client { + if tenantID == "" || tenantID == d.config.TenantID { + return d.client + } + d.mu.Lock() + defer d.mu.Unlock() + if c, ok := d.clientCache[tenantID]; ok { + return c + } + // Same shortCode/base-URL as the datasource; only the tenant (org path + scope) changes. + c := metrics.NewClient(metrics.Config{ + ShortCode: d.config.ShortCode, + TenantID: tenantID, + BaseURL: d.config.ApiURL, // preserve mock/demo base URL when configured + }, d.auth) + d.clientCache[tenantID] = c + return c +} + +// DatasourceConfig holds the non-sensitive configuration from jsonData. +type DatasourceConfig struct { + ShortCode string `json:"shortCode"` + TenantID string `json:"tenantId"` + ClientID string `json:"clientId"` + AccountManagerHost string `json:"accountManagerHost"` + ApiURL string `json:"apiUrl"` // Optional full Metrics API base URL (incl. /observability/metrics/v1) + TokenURL string `json:"tokenUrl"` // Optional full OAuth token endpoint URL +} + +// LabelFilter is a post-fetch filter applied to enriched series tags (the derived +// dimensions we compute: metricId, statusClass, cacheStatus, host, apiVersion, ...). +// Op is "=" or "!=" (default "="). +type LabelFilter struct { + Key string `json:"key"` + Op string `json:"op,omitempty"` + Value string `json:"value"` +} + +// QueryModel represents the query submitted from the frontend. +type QueryModel struct { + RefID string `json:"refId"` + Category string `json:"category"` // "overall", "scapi", "ocapi", etc. + + // MetricIDs selects which metrics to return (e.g. ["requestLatency"]). Empty = all + // metrics in the category. Applied post-fetch. Multi-select per the editor. + MetricIDs []string `json:"metricIds,omitempty"` + + // Push-down (server) filters — sent to the API as query params. Validated against + // the server enum by the editor. These also cause the API to drill down. + APIFamily string `json:"apiFamily,omitempty"` + APIName string `json:"apiName,omitempty"` + OcapiCategory string `json:"ocapiCategory,omitempty"` + OcapiAPI string `json:"ocapiApi,omitempty"` + ThirdPartyServiceID string `json:"thirdPartyServiceId,omitempty"` + + // LabelFilters — post-fetch filters on enriched tags (the tiered "Label filters"). + LabelFilters []LabelFilter `json:"labelFilters,omitempty"` + + // GroupBy — label keys used to build the per-series display name / legend. + // Empty = use all dimension tags (minus realm/environment identity). + GroupBy []string `json:"groupBy,omitempty"` + + // Optional per-query tenant override (empty = datasource default). Tenants on the + // same realm share the datasource's shortCode + OAuth client; only the tenant + // (org path + per-tenant scope) changes. Enables a dashboard $tenant variable to + // switch between e.g. prd and stg. Effective only if the datasource's client is + // authorized for the requested tenant's scope. + TenantID string `json:"tenantId,omitempty"` +} + +// NewDatasource creates a new datasource instance. +func NewDatasource(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + // Parse jsonData + var config DatasourceConfig + if err := json.Unmarshal(settings.JSONData, &config); err != nil { + return nil, fmt.Errorf("failed to parse datasource settings: %w", err) + } + + // Validate required fields + if config.ShortCode == "" { + return nil, fmt.Errorf("shortCode is required") + } + if config.TenantID == "" { + return nil, fmt.Errorf("tenantId is required") + } + + // clientId is non-secret config (jsonData); only clientSecret is stored encrypted. + clientID := config.ClientID + clientSecret := settings.DecryptedSecureJSONData["clientSecret"] + if clientID == "" || clientSecret == "" { + return nil, fmt.Errorf("missing client credentials (clientId in jsonData and clientSecret in secureJsonData required)") + } + + // Create OAuth strategy + authCfg := auth.OAuthConfig{ + ClientID: clientID, + ClientSecret: clientSecret, + AccountManagerHost: config.AccountManagerHost, + TokenURL: config.TokenURL, // Optional override + } + if authCfg.AccountManagerHost == "" && authCfg.TokenURL == "" { + authCfg.AccountManagerHost = auth.DefaultAccountManagerHost + } + authStrategy := auth.NewOAuthStrategy(authCfg) + + // Create metrics client + client := metrics.NewClient(metrics.Config{ + ShortCode: config.ShortCode, + TenantID: config.TenantID, + BaseURL: config.ApiURL, // Optional override + }, authStrategy) + + return &B2CMetricsDatasource{ + client: client, + auth: authStrategy, + config: config, + clientCache: make(map[string]*metrics.Client), + }, nil +} + +// Dispose cleans up resources. +func (d *B2CMetricsDatasource) Dispose() { + // No cleanup needed for now +} + +// QueryData handles data queries from Grafana. +func (d *B2CMetricsDatasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + response := backend.NewQueryDataResponse() + + // Process each query. Responses are keyed by the authoritative query.RefID (from the + // Grafana request), never the RefID inside the query JSON, so a missing/inconsistent + // JSON refId can't misroute or drop a panel's response. + for _, query := range req.Queries { + refID := query.RefID + var qm QueryModel + if err := json.Unmarshal(query.JSON, &qm); err != nil { + response.Responses[refID] = backend.ErrDataResponse(backend.StatusBadRequest, fmt.Sprintf("failed to parse query: %v", err)) + continue + } + + // Validate category + if qm.Category == "" { + response.Responses[refID] = backend.ErrDataResponse(backend.StatusBadRequest, "category is required") + continue + } + + // Resolve time window (enforce retention and defaults) + window, err := metricsops.ResolveMetricsWindow(metricsops.MetricsWindowInput{ + From: query.TimeRange.From.UnixMilli(), + To: query.TimeRange.To.UnixMilli(), + }, time.Now()) + if err != nil { + response.Responses[refID] = backend.ErrDataResponse(backend.StatusBadRequest, fmt.Sprintf("invalid time window: %v", err)) + continue + } + + // Push-down (server) filters go to the API; also selects the client for any + // per-query tenant/shortCode override. + filters := buildFilters(qm) + client := d.clientFor(qm.TenantID) + + // Fetch metrics for the category + var metricsData *metrics.MetricsDataResponse + switch qm.Category { + case "overall": + metricsData, err = client.GetOverallMetrics(ctx, window.From, window.To, filters) + case "sales": + metricsData, err = client.GetSalesMetrics(ctx, window.From, window.To, filters) + case "ecdn": + metricsData, err = client.GetEcdnMetrics(ctx, window.From, window.To, filters) + case "third-party": + metricsData, err = client.GetThirdPartyMetrics(ctx, window.From, window.To, filters) + case "scapi": + metricsData, err = client.GetScapiMetrics(ctx, window.From, window.To, filters) + case "scapi-hooks": + metricsData, err = client.GetScapiHooksMetrics(ctx, window.From, window.To, filters) + case "mrt": + metricsData, err = client.GetMrtMetrics(ctx, window.From, window.To, filters) + case "controller": + metricsData, err = client.GetControllerMetrics(ctx, window.From, window.To, filters) + case "ocapi": + metricsData, err = client.GetOcapiMetrics(ctx, window.From, window.To, filters) + default: + err = fmt.Errorf("unknown category: %s", qm.Category) + } + + if err != nil { + response.Responses[refID] = mapMetricsError(err) + continue + } + + // Post-fetch: filter by selected metricIds + label filters (the tiered + // "Label filters"), then convert to Grafana frames. + filtered := applyPostFetchFilters(metricsData, qm) + frames := convertToFrames(filtered, qm, window) + response.Responses[refID] = backend.DataResponse{Frames: frames} + } + + return response, nil +} + +// mapMetricsError converts a Metrics client error into a DataResponse with the correct +// Grafana status + error source. Rate-limit (429) and 5xx are attributed to the +// downstream API (so they don't count against the plugin's own reliability and the UI +// shows a rate-limit/downstream message); other API 4xx are downstream client-config +// issues; anything untyped is treated as a plugin error. +func mapMetricsError(err error) backend.DataResponse { + if he, ok := metrics.AsHTTPError(err); ok { + msg := he.Error() + switch { + case he.IsRateLimit(): + return backend.ErrDataResponseWithSource(backend.StatusTooManyRequests, backend.ErrorSourceDownstream, + "B2C Metrics API rate limit exceeded — reduce dashboard refresh/panel count or widen the interval. "+msg) + case he.StatusCode >= 500: + return backend.ErrDataResponseWithSource(backend.StatusBadGateway, backend.ErrorSourceDownstream, msg) + case he.StatusCode >= 400: + // 4xx (bad request/auth/scope) — downstream config, not a plugin bug. + return backend.ErrDataResponseWithSource(backend.StatusBadRequest, backend.ErrorSourceDownstream, msg) + } + } + // Untyped (network, decode, unknown category) — attribute to the plugin. + return backend.ErrDataResponse(backend.StatusInternal, fmt.Sprintf("metrics query failed: %v", err)) +} + +// buildFilters constructs the filter map from query model. +func buildFilters(qm QueryModel) map[string]string { + filters := make(map[string]string) + + if qm.APIFamily != "" { + filters["apiFamily"] = qm.APIFamily + } + if qm.APIName != "" { + filters["apiName"] = qm.APIName + } + if qm.OcapiCategory != "" { + filters["ocapiCategory"] = qm.OcapiCategory + } + if qm.OcapiAPI != "" { + filters["ocapiApi"] = qm.OcapiAPI + } + if qm.ThirdPartyServiceID != "" { + filters["thirdPartyServiceId"] = qm.ThirdPartyServiceID + } + + return filters +} + +// applyPostFetchFilters narrows the API response by the selected metricIds and the +// tiered "Label filters" (post-fetch filters on enriched tags). metricId is matched +// against the parent Metric; all other keys are matched against the series' enriched +// tags. Empty metricIds means "all metrics". Returns a new response; input untouched. +func applyPostFetchFilters(resp *metrics.MetricsDataResponse, qm QueryModel) *metrics.MetricsDataResponse { + if len(qm.MetricIDs) == 0 && len(qm.LabelFilters) == 0 { + return resp + } + + metricWanted := map[string]bool{} + for _, m := range qm.MetricIDs { + if m != "" { + metricWanted[m] = true + } + } + + out := &metrics.MetricsDataResponse{} + for _, metric := range resp.Data { + if len(metricWanted) > 0 && !metricWanted[metric.MetricID] { + continue + } + kept := metric // copy metric header + kept.DataSeries = nil // rebuild series slice + for _, series := range metric.DataSeries { + if seriesMatchesLabelFilters(metric.MetricID, series.Tags, qm.LabelFilters) { + kept.DataSeries = append(kept.DataSeries, series) + } + } + if len(kept.DataSeries) > 0 { + out.Data = append(out.Data, kept) + } + } + return out +} + +// seriesMatchesLabelFilters reports whether a series satisfies every label filter. +// The virtual key "metricId" matches the parent metric id; every other key matches +// the enriched series tags. Op "!=" negates; anything else is treated as "=". +func seriesMatchesLabelFilters(metricID string, tags metricsops.MetricSeriesTags, filters []LabelFilter) bool { + for _, f := range filters { + if f.Key == "" { + continue + } + var actual string + if f.Key == "metricId" { + actual = metricID + } else { + actual = tags[f.Key] + } + eq := actual == f.Value + if f.Op == "!=" { + if eq { + return false + } + } else if !eq { + return false + } + } + return true +} + +// preparedSeries is an intermediate representation of one series while frame naming +// is computed across the whole result set (collision-aware). +type preparedSeries struct { + metricID string + metricUnit string + labels data.Labels + times []time.Time + values []float64 +} + +// convertToFrames converts a Metrics API response to Grafana data frames. +// +// Naming is computed in two passes so legends are pretty AND unambiguous: +// 1. Each series gets a name from the group-by values (or all dimensions when no +// group-by is set). Zero-point series are dropped (e.g. empty rollup series). +// 2. Series whose names collide are disambiguated by appending their remaining +// distinguishing label values — so "product" HIT vs MISS become "product (HIT)" +// and "product (MISS)" rather than two identical rows. +// +// metricId is merged into each series' labels so it is a first-class, groupable/ +// filterable dimension and remains available for a ${__field.labels.metricId} +// display-name override in the panel. +func convertToFrames(metricsData *metrics.MetricsDataResponse, qm QueryModel, window *metricsops.ResolvedMetricsWindow) data.Frames { + // Pass 1: materialize series, skipping those with no data points in the window. + var prepared []preparedSeries + for _, metric := range metricsData.Data { + for _, series := range metric.DataSeries { + if !hasNonNullData(series.Data) { + continue // drop empty/zero-point series (e.g. inactive rollups) + } + times := make([]time.Time, len(series.Data)) + values := make([]float64, len(series.Data)) + for i, point := range series.Data { + times[i] = time.UnixMilli(point.Timestamp) + values[i] = point.Value + } + labels := cloneLabels(series.Tags) + labels["metricId"] = metric.MetricID + prepared = append(prepared, preparedSeries{ + metricID: metric.MetricID, metricUnit: metric.Unit, labels: labels, times: times, values: values, + }) + } + } + + // Pass 2: compute base names and detect collisions. + baseNames := make([]string, len(prepared)) + nameCounts := make(map[string]int) + for i, ps := range prepared { + baseNames[i] = baseSeriesName(ps.metricID, metricTitleFor(metricsData, ps.metricID), ps.labels, qm.GroupBy) + nameCounts[baseNames[i]]++ + } + + frames := make(data.Frames, 0, len(prepared)) + finalCounts := make(map[string]int) + for i, ps := range prepared { + name := baseNames[i] + if nameCounts[name] > 1 { + // Ambiguous: append the distinguishing labels (those NOT already in the name). + if extra := distinguishingSuffix(ps.labels, qm.GroupBy); extra != "" { + name = fmt.Sprintf("%s (%s)", name, extra) + } + } + // Last-resort uniqueness: if two series are still identically named (truly + // identical labels), append an occurrence index so Grafana keeps them distinct. + finalCounts[name]++ + if k := finalCounts[name]; k > 1 { + ps.labels["seriesIndex"] = fmt.Sprintf("%d", k) + name = fmt.Sprintf("%s #%d", name, k) + } + + frame := data.NewFrame(name, + data.NewField("time", nil, ps.times), + data.NewField(ps.metricID, ps.labels, ps.values), + ) + frame.Fields[1].Config = &data.FieldConfig{DisplayNameFromDS: name} + if ps.metricUnit != "" { + frame.Fields[1].Config.Unit = normalizeUnit(ps.metricUnit) + } + frame.Meta = &data.FrameMeta{ + ExecutedQueryString: fmt.Sprintf("category=%s from=%s to=%s", qm.Category, window.From.Format(time.RFC3339), window.To.Format(time.RFC3339)), + } + if window.ClampedFrom { + frame.Meta.Notices = []data.Notice{{ + Severity: data.NoticeSeverityWarning, + Text: "Time range was clamped to 30-day retention window", + }} + } + frames = append(frames, frame) + } + + return frames +} + +// cloneLabels returns a shallow copy of a tag map as data.Labels. +func cloneLabels(tags map[string]string) data.Labels { + out := make(data.Labels, len(tags)+1) + for k, v := range tags { + out[k] = v + } + return out +} + +// nameDimensionOrder is the stable order of dimension tags used to build a display +// name when the query does not specify explicit group-by keys. Identity tags +// (realm/environment) and metricId are handled separately. +var nameDimensionOrder = []string{ + "apiFamily", "apiName", "apiVersion", "host", "controller", + "ocapiCategory", "ocapiApi", "statusClass", "cacheStatus", "exceptionType", + "aggregation", +} + +// hasNonNullData reports whether a series has at least one data point. The Metrics API +// can return rollup/aggregate series with an empty data array in a given window; those +// render as an empty legend row and are dropped. +func hasNonNullData(points []metrics.DataPoint) bool { + return len(points) > 0 +} + +// metricTitleFor returns the human title for a metricId from the response (falls back +// to the id itself). +func metricTitleFor(resp *metrics.MetricsDataResponse, metricID string) string { + for _, m := range resp.Data { + if m.MetricID == metricID { + if m.Title != "" { + return m.Title + } + return metricID + } + } + return metricID +} + +// baseSeriesName builds the pretty legend name for a series. With group-by keys, it is +// the join of those label values (Prometheus {{legend}} style). Without group-by, it is +// the metric title plus all present dimensions. A series that has none of the requested +// group-by labels (e.g. an aggregate rollup) is named by its non-identity dimensions, or +// the metric title as a last resort — never blank. +func baseSeriesName(metricID, metricTitle string, tags data.Labels, groupBy []string) string { + if len(groupBy) > 0 { + parts := make([]string, 0, len(groupBy)) + for _, k := range groupBy { + if v := tags[k]; v != "" { + parts = append(parts, v) + } + } + if len(parts) > 0 { + return strings.Join(parts, " · ") + } + // Series lacks every grouped label (e.g. rollup): name it by whatever + // distinguishing dimension it does carry, else the metric title. + if agg := tags["aggregation"]; agg != "" { + return fmt.Sprintf("%s (aggregation=%s)", metricTitle, agg) + } + if extra := dimensionValues(tags); extra != "" { + return fmt.Sprintf("%s (%s)", metricTitle, extra) + } + return metricTitle + } + + if extra := dimensionValues(tags); extra != "" { + return fmt.Sprintf("%s (%s)", metricTitle, extra) + } + if realm := tags["realm"]; realm != "" { + if env := tags["environment"]; env != "" { + return fmt.Sprintf("%s (%s/%s)", metricTitle, realm, env) + } + return fmt.Sprintf("%s (%s)", metricTitle, realm) + } + return metricTitle +} + +// dimensionValues joins a series' non-identity dimension values in a stable order. +func dimensionValues(tags data.Labels) string { + parts := []string{} + for _, k := range nameDimensionOrder { + if v := tags[k]; v != "" { + parts = append(parts, v) + } + } + return strings.Join(parts, ", ") +} + +// distinguishingSuffix returns the label values that distinguish a series but are NOT +// already part of its group-by name — used to disambiguate colliding legend names +// (e.g. two "product" series differing only by cacheStatus → suffix "HIT" / "MISS"). +func distinguishingSuffix(tags data.Labels, groupBy []string) string { + inGroup := map[string]bool{} + for _, k := range groupBy { + inGroup[k] = true + } + parts := []string{} + for _, k := range nameDimensionOrder { + if inGroup[k] { + continue + } + if v := tags[k]; v != "" { + parts = append(parts, v) + } + } + return strings.Join(parts, ", ") +} + +// normalizeUnit maps a Metrics API unit string to a Grafana unit id. The API uses +// "s" for seconds (which Grafana also understands); other/empty units pass through. +func normalizeUnit(apiUnit string) string { + switch apiUnit { + case "s": + return "s" // seconds + case "ms": + return "ms" // milliseconds + default: + return apiUnit + } +} + +// CheckHealth performs a health check of the datasource. +func (d *B2CMetricsDatasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + log.DefaultLogger.Info("Checking datasource health") + + // Try to fetch a small amount of data to verify connectivity + window, err := metricsops.ResolveMetricsWindow(metricsops.MetricsWindowInput{ + Window: "5m", // Just last 5 minutes + }, time.Now()) + if err != nil { + return &backend.CheckHealthResult{ + Status: backend.HealthStatusError, + Message: fmt.Sprintf("Failed to resolve time window: %v", err), + }, nil + } + + // Try to fetch overall metrics as a connectivity test + _, err = d.client.GetOverallMetrics(ctx, window.From, window.To, nil) + if err != nil { + return &backend.CheckHealthResult{ + Status: backend.HealthStatusError, + Message: fmt.Sprintf("Failed to connect to Metrics API: %v", err), + }, nil + } + + return &backend.CheckHealthResult{ + Status: backend.HealthStatusOk, + Message: fmt.Sprintf("Successfully connected to B2C Commerce Metrics API (tenant: %s)", d.config.TenantID), + }, nil +} + +// METRIC_CATEGORIES is the fixed set of Metrics API endpoints. +var metricCategories = []map[string]string{ + {"label": "Overall", "value": "overall"}, + {"label": "Sales", "value": "sales"}, + {"label": "eCDN", "value": "ecdn"}, + {"label": "Third-party", "value": "third-party"}, + {"label": "SCAPI", "value": "scapi"}, + {"label": "SCAPI Hooks", "value": "scapi-hooks"}, + {"label": "MRT", "value": "mrt"}, + {"label": "Controller", "value": "controller"}, + {"label": "OCAPI", "value": "ocapi"}, +} + +// pushDownFiltersByCategory maps each category to its server-side (push-down) filter +// keys — the ones the Metrics API accepts as query params. Their VALUES come from a +// fixed server enum (see pushDownEnum), not from the returned data. +var pushDownFiltersByCategory = map[string][]string{ + "scapi": {"apiFamily", "apiName"}, + "ocapi": {"ocapiCategory", "ocapiApi"}, + "third-party": {"thirdPartyServiceId"}, +} + +// pushDownEnum is the authoritative server-accepted value list for push-down filter +// keys whose values are a fixed enum (as reported by the API's 400 validation error). +// Keys absent here (e.g. apiName, thirdPartyServiceId) are free-form / discovered. +var pushDownEnum = map[string][]string{ + "apiFamily": {"shopper", "admin", "data", "cdn", "search", "orders", "customers", "products", "inventory", "pricing", "promotions", "content"}, + "ocapiCategory": {"shop", "data"}, +} + +// identityTagKeys are enrichment tags that identify the request, not a per-series +// dimension; excluded from the derived label-key list offered as label filters. +var identityTagKeys = map[string]bool{"realm": true, "environment": true, "seriesIndex": true} + +// CallResource serves the dynamic discovery API that powers the query editor: +// - GET categories → the 9 metric categories +// - GET metrics?category=&tenantId= → probed metricIds (+unit) for a category +// - GET push-down-filters?category= → server filter keys + enum values (tiered) +// - GET label-keys?category=&tenantId= → derived (post-fetch) label keys, probed +// - GET label-values?category=&key=&... → values for a label key (enum or probed) +func (d *B2CMetricsDatasource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { + parsed, _ := url.Parse(req.URL) + q := url.Values{} + if parsed != nil { + q = parsed.Query() + } + switch req.Path { + case "categories": + return sendJSON(sender, metricCategories) + case "push-down-filters": + return d.handlePushDownFilters(sender, q.Get("category")) + case "metrics": + return d.handleGetMetrics(ctx, sender, q.Get("category"), q.Get("tenantId")) + case "label-keys": + return d.handleLabelKeys(ctx, sender, q.Get("category"), q.Get("tenantId")) + case "label-values": + return d.handleLabelValues(ctx, sender, q.Get("category"), q.Get("key"), q.Get("tenantId")) + default: + return sender.Send(&backend.CallResourceResponse{ + Status: http.StatusNotFound, + Body: []byte(fmt.Sprintf("unknown resource path: %s", req.Path)), + }) + } +} + +// probeWindow fetches a recent slice of a category (last 24h — the API max) so the +// discovery handlers can read what metricIds/labels actually exist. Best-effort: +// returns nil on error so discovery degrades to empty rather than failing the editor. +func (d *B2CMetricsDatasource) probeCategory(ctx context.Context, category, tenantID string) (*metrics.MetricsDataResponse, error) { + if category == "" { + return nil, fmt.Errorf("category is required") + } + window, err := metricsops.ResolveMetricsWindow(metricsops.MetricsWindowInput{Window: "24h"}, time.Now()) + if err != nil { + return nil, err + } + client := d.clientFor(tenantID) + var resp *metrics.MetricsDataResponse + switch category { + case "overall": + resp, err = client.GetOverallMetrics(ctx, window.From, window.To, nil) + case "sales": + resp, err = client.GetSalesMetrics(ctx, window.From, window.To, nil) + case "ecdn": + resp, err = client.GetEcdnMetrics(ctx, window.From, window.To, nil) + case "third-party": + resp, err = client.GetThirdPartyMetrics(ctx, window.From, window.To, nil) + case "scapi": + resp, err = client.GetScapiMetrics(ctx, window.From, window.To, nil) + case "scapi-hooks": + resp, err = client.GetScapiHooksMetrics(ctx, window.From, window.To, nil) + case "mrt": + resp, err = client.GetMrtMetrics(ctx, window.From, window.To, nil) + case "controller": + resp, err = client.GetControllerMetrics(ctx, window.From, window.To, nil) + case "ocapi": + resp, err = client.GetOcapiMetrics(ctx, window.From, window.To, nil) + default: + return nil, fmt.Errorf("unknown category: %s", category) + } + if err != nil { + log.DefaultLogger.Warn("discovery probe failed", "category", category, "error", err) + return nil, err + } + return resp, nil +} + +// handleGetMetrics returns the metricIds available in a category (probed live), each +// with its unit, so the editor's Metric multi-select is accurate and unit-aware. +func (d *B2CMetricsDatasource) handleGetMetrics(ctx context.Context, sender backend.CallResourceResponseSender, category, tenantID string) error { + type metricOpt struct { + Value string `json:"value"` + Label string `json:"label"` + Unit string `json:"unit"` + } + resp, err := d.probeCategory(ctx, category, tenantID) + if err != nil { + return sendResourceErr(sender, err) + } + opts := []metricOpt{} + seen := map[string]bool{} + for _, m := range resp.Data { + if seen[m.MetricID] { + continue + } + seen[m.MetricID] = true + label := m.Title + if label == "" { + label = m.MetricID + } + opts = append(opts, metricOpt{Value: m.MetricID, Label: label, Unit: m.Unit}) + } + return sendJSON(sender, opts) +} + +// handlePushDownFilters returns the server-side (push-down) filter keys for a category, +// each flagged with whether its values come from a fixed enum. This is the tiered +// "Server filters" section of the editor. +func (d *B2CMetricsDatasource) handlePushDownFilters(sender backend.CallResourceResponseSender, category string) error { + type pushFilter struct { + Key string `json:"key"` + HasEnum bool `json:"hasEnum"` + Values []string `json:"values,omitempty"` + } + out := []pushFilter{} + for _, key := range pushDownFiltersByCategory[category] { + vals, hasEnum := pushDownEnum[key] + out = append(out, pushFilter{Key: key, HasEnum: hasEnum, Values: vals}) + } + return sendJSON(sender, out) +} + +// handleLabelKeys returns the derived (post-fetch) label keys present in a category, +// probed from live data. These populate the tiered "Label filters" / "Group by" pickers. +// metricId is always offered (it is a first-class label we add per series). +func (d *B2CMetricsDatasource) handleLabelKeys(ctx context.Context, sender backend.CallResourceResponseSender, category, tenantID string) error { + resp, err := d.probeCategory(ctx, category, tenantID) + if err != nil { + return sendResourceErr(sender, err) + } + keys := map[string]bool{"metricId": true} + for _, m := range resp.Data { + for _, s := range m.DataSeries { + for k := range s.Tags { + if !identityTagKeys[k] { + keys[k] = true + } + } + } + } + out := make([]string, 0, len(keys)) + for k := range keys { + out = append(out, k) + } + sort.Strings(out) + return sendJSON(sender, out) +} + +// handleLabelValues returns the distinct values for a label key. For push-down keys +// with a fixed server enum it returns that enum; otherwise it probes live data and +// collects the distinct enriched-tag values (Prometheus label_values() style). +func (d *B2CMetricsDatasource) handleLabelValues(ctx context.Context, sender backend.CallResourceResponseSender, category, key, tenantID string) error { + if key == "" { + return sendJSON(sender, []string{}) + } + if enum, ok := pushDownEnum[key]; ok { + return sendJSON(sender, enum) + } + resp, err := d.probeCategory(ctx, category, tenantID) + if err != nil { + return sendResourceErr(sender, err) + } + set := map[string]bool{} + for _, m := range resp.Data { + if key == "metricId" { + set[m.MetricID] = true + continue + } + for _, s := range m.DataSeries { + if v := s.Tags[key]; v != "" { + set[v] = true + } + } + } + out := make([]string, 0, len(set)) + for v := range set { + out = append(out, v) + } + sort.Strings(out) + return sendJSON(sender, out) +} + +// sendJSON marshals v and writes it as a 200 application/json CallResource response. +func sendJSON(sender backend.CallResourceResponseSender, v any) error { + body, err := json.Marshal(v) + if err != nil { + return err + } + return sender.Send(&backend.CallResourceResponse{ + Status: http.StatusOK, + Body: body, + Headers: map[string][]string{"Content-Type": {"application/json"}}, + }) +} + +// sendResourceErr surfaces a discovery/probe failure to the query editor as an HTTP +// error (rather than an empty dropdown), so a rate-limited or unreachable API is +// visible as an error the user can act on instead of looking like "no data". Rate +// limits map to 429 so the editor can show a distinct message. +func sendResourceErr(sender backend.CallResourceResponseSender, err error) error { + status := http.StatusBadGateway + if he, ok := metrics.AsHTTPError(err); ok && he.IsRateLimit() { + status = http.StatusTooManyRequests + } + return sender.Send(&backend.CallResourceResponse{ + Status: status, + Body: []byte(err.Error()), + }) +} + +// getTagKeys / getTagValues equivalents are exposed to the frontend via the +// label-keys / label-values resources above; the datasource.ts wires them into +// Grafana's ad-hoc filter and variable-query mechanisms. diff --git a/packages/b2c-grafana-datasource/provisioning-real/dashboards/dashboards.yaml b/packages/b2c-grafana-datasource/provisioning-real/dashboards/dashboards.yaml new file mode 100644 index 000000000..8bda925c6 --- /dev/null +++ b/packages/b2c-grafana-datasource/provisioning-real/dashboards/dashboards.yaml @@ -0,0 +1,10 @@ +apiVersion: 1 + +providers: + - name: B2C Commerce Dashboards + type: file + updateIntervalSeconds: 10 + allowUiUpdates: true + options: + path: /var/lib/grafana/dashboards + foldersFromFilesStructure: true diff --git a/packages/b2c-grafana-datasource/provisioning-real/datasources/b2c-metrics.yaml b/packages/b2c-grafana-datasource/provisioning-real/datasources/b2c-metrics.yaml new file mode 100644 index 000000000..eba3a9fd4 --- /dev/null +++ b/packages/b2c-grafana-datasource/provisioning-real/datasources/b2c-metrics.yaml @@ -0,0 +1,51 @@ +apiVersion: 1 + +# ============================================================================ +# REAL MODE: connects to the live SCAPI Observability Metrics API. +# Credentials are injected as environment variables at `docker compose up` time +# (see Makefile `real` target, which sources them from the b2c CLI keychain — +# no secrets are written to disk). Grafana interpolates ${VAR} from the +# container environment when provisioning this datasource. +# +# Same uid as the demo datasource (b2c-metrics-demo) so the existing demo +# dashboard renders real data unchanged. +# ============================================================================ +datasources: + - name: B2C Commerce Metrics (Real) + uid: b2c-metrics-demo + type: salesforce-b2c-metrics-datasource + access: proxy + isDefault: true + editable: true + jsonData: + # Empty apiUrl/tokenUrl → the backend derives the real production URLs: + # https://{shortCode}.api.commercecloud.salesforce.com/observability/metrics/v1 + # https://{accountManagerHost}/dwsso/oauth2/access_token + apiUrl: "" + tokenUrl: "" + shortCode: "${B2C_SHORT_CODE}" + tenantId: "${B2C_TENANT_ID}" + clientId: "${B2C_CLIENT_ID}" + accountManagerHost: "${B2C_ACCOUNT_MANAGER_HOST}" + secureJsonData: + clientSecret: "${B2C_CLIENT_SECRET}" + version: 1 + + # ============================================================================ + # CIP (Commerce Intelligence Platform) — raw Calcite SQL over the analytics + # warehouse. Real-mode only (Avatica/protobuf can't be mocked like REST). + # Shares the same OAuth client; instance = tenant id (e.g. bdpx_prd). + # ============================================================================ + - name: B2C Commerce Intelligence (CIP) + uid: b2c-cip + type: salesforce-b2c-cip-datasource + access: proxy + isDefault: false + editable: true + jsonData: + instance: "${B2C_TENANT_ID}" + clientId: "${B2C_CLIENT_ID}" + accountManagerHost: "${B2C_ACCOUNT_MANAGER_HOST}" + secureJsonData: + clientSecret: "${B2C_CLIENT_SECRET}" + version: 1 diff --git a/packages/b2c-grafana-datasource/provisioning/dashboards/dashboards.yaml b/packages/b2c-grafana-datasource/provisioning/dashboards/dashboards.yaml new file mode 100644 index 000000000..8bda925c6 --- /dev/null +++ b/packages/b2c-grafana-datasource/provisioning/dashboards/dashboards.yaml @@ -0,0 +1,10 @@ +apiVersion: 1 + +providers: + - name: B2C Commerce Dashboards + type: file + updateIntervalSeconds: 10 + allowUiUpdates: true + options: + path: /var/lib/grafana/dashboards + foldersFromFilesStructure: true diff --git a/packages/b2c-grafana-datasource/provisioning/datasources/b2c-metrics.yaml b/packages/b2c-grafana-datasource/provisioning/datasources/b2c-metrics.yaml new file mode 100644 index 000000000..a0f194619 --- /dev/null +++ b/packages/b2c-grafana-datasource/provisioning/datasources/b2c-metrics.yaml @@ -0,0 +1,44 @@ +apiVersion: 1 + +datasources: + # ============================================================================ + # DEMO MODE: Uses mock-metrics service with synthetic data + # ============================================================================ + - name: B2C Commerce Metrics (Demo) + uid: b2c-metrics-demo + type: salesforce-b2c-metrics-datasource + access: proxy + isDefault: true + editable: true + jsonData: + # Mock service URLs (point to mock-metrics container) + apiUrl: "http://mock-metrics:8080/observability/metrics/v1" + tokenUrl: "http://mock-metrics:8080/dwsso/oauth2/access_token" + # Demo credentials (accepted by mock) + shortCode: "demo" + tenantId: "f_ecom_bdpx_prd" + clientId: "demo" + secureJsonData: + clientSecret: "demo" + version: 1 + + # ============================================================================ + # PRODUCTION MODE (commented out - uncomment and configure for real tenant) + # ============================================================================ + # - name: B2C Commerce Metrics (Production) + # uid: b2c-metrics-prod + # type: salesforce-b2c-metrics-datasource + # access: proxy + # isDefault: false + # editable: true + # jsonData: + # # Production URLs (leave empty to derive from shortCode/accountManagerHost) + # # apiUrl: "" # Empty → https://{shortCode}.api.commercecloud.salesforce.com/observability/metrics/v1 + # # tokenUrl: "" # Empty → https://{accountManagerHost}/dwsso/oauth2/access_token + # shortCode: "${B2C_SHORT_CODE}" # e.g., "zzrj-001" + # tenantId: "${B2C_TENANT_ID}" # e.g., "f_ecom_zzrj_prd" + # clientId: "${B2C_CLIENT_ID}" # API Client ID + # accountManagerHost: "account.demandware.com" # Optional (defaults to account.demandware.com) + # secureJsonData: + # clientSecret: "${B2C_CLIENT_SECRET}" # API Client Secret (encrypted) + # version: 1 diff --git a/packages/b2c-grafana-datasource/src/ConfigEditor.tsx b/packages/b2c-grafana-datasource/src/ConfigEditor.tsx new file mode 100644 index 000000000..cf30edf7d --- /dev/null +++ b/packages/b2c-grafana-datasource/src/ConfigEditor.tsx @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import React, { ChangeEvent } from 'react'; +import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; +import { Field, Input, SecretInput } from '@grafana/ui'; + +import { B2CMetricsDataSourceOptions, B2CMetricsSecureJsonData } from './types'; + +interface Props extends DataSourcePluginOptionsEditorProps {} + +/** + * ConfigEditor component for B2C Metrics datasource + * + * Provides form fields for: + * - shortCode (instance short code) + * - tenantId (organization/tenant identifier) + * - clientId (OAuth client ID) + * - accountManagerHost (optional, defaults to SDK value) + * - clientSecret (secure field) + */ +export function ConfigEditor(props: Props) { + const { onOptionsChange, options } = props; + const { jsonData, secureJsonFields, secureJsonData } = options; + + // Handler for jsonData field changes + const onJsonDataChange = (key: K) => { + return (event: ChangeEvent) => { + onOptionsChange({ + ...options, + jsonData: { + ...jsonData, + [key]: event.target.value, + }, + }); + }; + }; + + // Handler for secureJsonData field changes + const onSecretChange = (event: ChangeEvent) => { + onOptionsChange({ + ...options, + secureJsonData: { + ...secureJsonData, + clientSecret: event.target.value, + }, + }); + }; + + // Handler for reset secure field + const onResetSecret = () => { + onOptionsChange({ + ...options, + secureJsonFields: { + ...secureJsonFields, + clientSecret: false, + }, + secureJsonData: { + ...secureJsonData, + clientSecret: '', + }, + }); + }; + + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +} diff --git a/packages/b2c-grafana-datasource/src/QueryEditor.tsx b/packages/b2c-grafana-datasource/src/QueryEditor.tsx new file mode 100644 index 000000000..afb14df3f --- /dev/null +++ b/packages/b2c-grafana-datasource/src/QueryEditor.tsx @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import React, { useEffect, useState } from 'react'; +import { QueryEditorProps, SelectableValue } from '@grafana/data'; +import { getTemplateSrv } from '@grafana/runtime'; +import { Button, Field, InlineField, InlineFieldRow, MultiSelect, Select } from '@grafana/ui'; + +import { B2CMetricsDataSource } from './datasource'; +import { + B2CMetricsDataSourceOptions, + B2CMetricsQuery, + LabelFilter, + METRIC_CATEGORIES, + MetricCategory, + MetricOption, + PushDownFilter, +} from './types'; + +type Props = QueryEditorProps; + +const CATEGORY_OPTIONS: Array> = METRIC_CATEGORIES.map((c) => ({ + label: c, + value: c, +})); + +const OP_OPTIONS: Array { + let active = true; + if (!category) { + return; + } + const tenant = discoveryTenant; + Promise.all([ + datasource.getMetricOptions(category, tenant), + datasource.getPushDownFilters(category), + datasource.getLabelKeys(category, tenant), + ]) + .then(([metrics, pd, keys]) => { + if (!active) { + return; + } + setMetricOpts(metrics); + setPushDown(pd); + setLabelKeys(keys); + }) + .catch((e) => console.error('discovery load failed', e)); + return () => { + active = false; + }; + }, [datasource, category, discoveryTenant]); + + const patch = (partial: Partial, run = true) => { + onChange({ ...query, ...partial }); + if (run) { + onRunQuery(); + } + }; + + const onCategoryChange = (v: SelectableValue) => { + // Reset all metric/filter selections — they are category-specific. + onChange({ + ...query, + category: v.value as MetricCategory, + metricIds: [], + apiFamily: undefined, + apiName: undefined, + ocapiCategory: undefined, + ocapiApi: undefined, + thirdPartyServiceId: undefined, + labelFilters: [], + groupBy: [], + }); + // Discovered label values are category-specific; clear the cache so a filter with + // the same key in the new category doesn't show stale values from the old one. + setValueCache({}); + onRunQuery(); + }; + + // ---- Server (push-down) filters ----------------------------------------- + const pushDownKeyToField: Record = { + apiFamily: 'apiFamily', + apiName: 'apiName', + ocapiCategory: 'ocapiCategory', + ocapiApi: 'ocapiApi', + thirdPartyServiceId: 'thirdPartyServiceId', + }; + + const renderPushDownFilter = (f: PushDownFilter) => { + const field = pushDownKeyToField[f.key]; + const current = (query[field] as string | undefined) || ''; + const enumOptions: Array> = (f.values ?? []).map((v) => ({ label: v, value: v })); + return ( + + + + + ({ label: `${m.label}${m.unit ? ` (${m.unit})` : ''}`, value: m.value }))} + value={(query.metricIds ?? []).map((v) => ({ label: v, value: v }))} + onChange={(opts) => patch({ metricIds: opts.map((o) => o.value as string) })} + /> + + + + {pushDown.length > 0 && ( + + {pushDown.map(renderPushDownFilter)} + + )} + + +
+ {labelFilters.map((f, idx) => ( + + + updateLabelFilter(idx, { op: (opt?.value as '=' | '!=') || '=' })} + /> + + + + + + + + + + + + + + + + + + + +
+ ); +} diff --git a/packages/b2c-grafana-datasource/src/cip/QueryEditor.tsx b/packages/b2c-grafana-datasource/src/cip/QueryEditor.tsx new file mode 100644 index 000000000..d0629cd7d --- /dev/null +++ b/packages/b2c-grafana-datasource/src/cip/QueryEditor.tsx @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import React, { useEffect, useState } from 'react'; +import { QueryEditorProps, SelectableValue } from '@grafana/data'; +import { CodeEditor, InlineField, InlineFieldRow, Select, Button, Collapse } from '@grafana/ui'; + +import { CIPDataSource } from './datasource'; +import { CIPColumn, CIPDataSourceOptions, CIPFormat, CIPQuery, CIPTable } from './types'; + +type Props = QueryEditorProps; + +const FORMAT_OPTIONS: Array> = [ + { label: 'Time series', value: CIPFormat.TimeSeries }, + { label: 'Table', value: CIPFormat.Table }, +]; + +export function QueryEditor(props: Props) { + const { datasource, query, onChange, onRunQuery } = props; + + const [tables, setTables] = useState([]); + const [openSchema, setOpenSchema] = useState(false); + const [expandedTable, setExpandedTable] = useState(''); + const [columns, setColumns] = useState>({}); + + useEffect(() => { + if (openSchema && tables.length === 0) { + datasource + .getTables('warehouse') + .then(setTables) + .catch((e) => console.error('CIP tables load failed', e)); + } + }, [openSchema, tables.length, datasource]); + + const onSqlChange = (rawSql: string) => onChange({ ...query, rawSql }); + const onFormatChange = (v: SelectableValue) => { + onChange({ ...query, format: v.value ?? CIPFormat.TimeSeries }); + onRunQuery(); + }; + + const toggleTable = (t: CIPTable) => { + const key = `${t.schema}.${t.name}`; + if (expandedTable === key) { + setExpandedTable(''); + return; + } + setExpandedTable(key); + if (!columns[key]) { + datasource + .getColumns(t.schema, t.name) + .then((cols) => setColumns((c) => ({ ...c, [key]: cols }))) + .catch((e) => console.error('CIP columns load failed', e)); + } + }; + + return ( +
+ + + setValue(e.currentTarget.value)} + onBlur={onBlur} + /> + + ); +} diff --git a/packages/b2c-grafana-datasource/src/cip/datasource.ts b/packages/b2c-grafana-datasource/src/cip/datasource.ts new file mode 100644 index 000000000..469814713 --- /dev/null +++ b/packages/b2c-grafana-datasource/src/cip/datasource.ts @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import { DataSourceInstanceSettings, MetricFindValue, ScopedVars } from '@grafana/data'; +import { DataSourceWithBackend, getTemplateSrv } from '@grafana/runtime'; + +import { CIPColumn, CIPDataSourceOptions, CIPQuery, CIPTable, CIPVariableQuery, DEFAULT_CIP_QUERY } from './types'; + +/** + * CIP SQL datasource. Delegates query execution to the Go backend (which owns OAuth, + * the Avatica/protobuf transport, macro expansion, and frame construction). + */ +export class CIPDataSource extends DataSourceWithBackend { + constructor(instanceSettings: DataSourceInstanceSettings) { + super(instanceSettings); + } + + getDefaultQuery(): Partial { + return DEFAULT_CIP_QUERY; + } + + filterQuery(query: CIPQuery): boolean { + return Boolean(query.rawSql && query.rawSql.trim()); + } + + applyTemplateVariables(query: CIPQuery, scopedVars: ScopedVars): CIPQuery { + return { + ...query, + rawSql: query.rawSql ? getTemplateSrv().replace(query.rawSql, scopedVars) : query.rawSql, + }; + } + + /** Schema browser: list catalog tables (optionally filtered by schema). */ + async getTables(schema?: string): Promise { + return (await this.getResource('tables', schema ? { schema } : {})) as CIPTable[]; + } + + /** Schema browser: describe a table's columns. */ + async getColumns(schema: string, table: string): Promise { + return (await this.getResource('columns', { schema, table })) as CIPColumn[]; + } + + /** + * Powers dashboard template variables. A variable query is either the canned + * keyword `sites` (distinct storefront site ids) or raw SQL whose first column's + * distinct values become the variable options. Template variables in the SQL are + * interpolated first, so variables can depend on each other. + */ + async metricFindQuery(query: CIPVariableQuery | string): Promise { + const raw = typeof query === 'string' ? query : query?.query ?? ''; + const trimmed = raw.trim(); + if (trimmed === '' || trimmed.toLowerCase() === 'sites') { + const sites = (await this.getResource('sites')) as string[]; + return sites.map((s) => ({ text: s, value: s })); + } + const sql = getTemplateSrv().replace(trimmed); + const values = (await this.getResource('variable', { sql })) as string[]; + return values.map((v) => ({ text: v, value: v })); + } +} diff --git a/packages/b2c-grafana-datasource/src/cip/img/logo.svg b/packages/b2c-grafana-datasource/src/cip/img/logo.svg new file mode 100644 index 000000000..fefa1e9ca --- /dev/null +++ b/packages/b2c-grafana-datasource/src/cip/img/logo.svg @@ -0,0 +1,7 @@ + + + + + + B2C + diff --git a/packages/b2c-grafana-datasource/src/cip/module.ts b/packages/b2c-grafana-datasource/src/cip/module.ts new file mode 100644 index 000000000..4600d4bce --- /dev/null +++ b/packages/b2c-grafana-datasource/src/cip/module.ts @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import { DataSourcePlugin } from '@grafana/data'; + +import { ConfigEditor } from './ConfigEditor'; +import { CIPDataSource } from './datasource'; +import { QueryEditor } from './QueryEditor'; +import { CIPDataSourceOptions, CIPQuery } from './types'; +import { VariableQueryEditor } from './VariableQueryEditor'; + +export const plugin = new DataSourcePlugin(CIPDataSource) + .setConfigEditor(ConfigEditor) + .setQueryEditor(QueryEditor) + .setVariableQueryEditor(VariableQueryEditor); diff --git a/packages/b2c-grafana-datasource/src/cip/plugin.json b/packages/b2c-grafana-datasource/src/cip/plugin.json new file mode 100644 index 000000000..a41572a72 --- /dev/null +++ b/packages/b2c-grafana-datasource/src/cip/plugin.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://raw.githubusercontent.com/grafana/grafana/master/docs/sources/developers/plugins/plugin.schema.json", + "type": "datasource", + "name": "Salesforce B2C Commerce Intelligence (CIP)", + "id": "salesforce-b2c-cip-datasource", + "backend": true, + "executable": "gpx_b2c_cip", + "alerting": true, + "metrics": true, + "annotations": false, + "logs": false, + "tracing": false, + "info": { + "version": "0.1.0", + "description": "Query the B2C Commerce Intelligence Platform (CIP) analytics warehouse with raw Calcite SQL and Grafana time macros.", + "author": { + "name": "Salesforce", + "url": "https://developer.salesforce.com/docs/commerce/commerce-api" + }, + "keywords": [ + "salesforce", + "b2c commerce", + "cip", + "analytics", + "sql", + "warehouse" + ], + "logos": { + "small": "img/logo.svg", + "large": "img/logo.svg" + }, + "links": [ + { + "name": "Documentation", + "url": "https://github.com/SalesforceCommerceCloud/b2c-developer-tooling" + } + ], + "screenshots": [], + "updated": "2026-07-14" + }, + "routes": [], + "dependencies": { + "grafanaDependency": ">=9.0.0", + "plugins": [] + } +} diff --git a/packages/b2c-grafana-datasource/src/cip/types.ts b/packages/b2c-grafana-datasource/src/cip/types.ts new file mode 100644 index 000000000..d79bea40b --- /dev/null +++ b/packages/b2c-grafana-datasource/src/cip/types.ts @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import { DataQuery, DataSourceJsonData } from '@grafana/data'; + +/** + * CIP query format, matching sqlds/sqlutil FormatQueryOption (numeric enum). + * TimeSeries (0) → sqlds reshapes long→wide for per-series charts; Table (1) → columns as-is. + */ +export enum CIPFormat { + TimeSeries = 0, + Table = 1, +} + +/** Query model for the CIP SQL datasource. */ +export interface CIPQuery extends DataQuery { + /** Raw Calcite SQL. Supports Grafana time macros ($__timeFilter, $__timeGroup, …). */ + rawSql: string; + /** Result shape. Numeric enum required by the sqlds backend (see {@link CIPFormat}). */ + format?: CIPFormat; +} + +export const DEFAULT_CIP_QUERY: Partial = { + rawSql: + 'SELECT $__timeGroupAlias(submit_date, $__interval),\n SUM(num_orders) AS orders\nFROM warehouse.ccdw_aggr_sales_summary\nWHERE $__timeFilter(submit_date)\nGROUP BY 1\nORDER BY 1', + format: CIPFormat.TimeSeries, +}; + +/** DataSource configuration (jsonData). */ +export interface CIPDataSourceOptions extends DataSourceJsonData { + /** CIP instance id (e.g. "bdpx_prd"). Falls back to tenantId when empty. */ + instance?: string; + tenantId?: string; + clientId: string; + accountManagerHost?: string; + /** Optional CIP Avatica host override (staging analytics). */ + cipHost?: string; +} + +/** Secure configuration (secureJsonData). */ +export interface CIPSecureJsonData { + clientSecret?: string; +} + +/** + * Template-variable query. `query` is either the keyword `sites` (distinct storefront + * site ids) or raw Calcite SQL whose first column's distinct values become the options. + */ +export interface CIPVariableQuery { + query: string; +} + +/** Table returned by the `tables` resource. */ +export interface CIPTable { + schema: string; + name: string; + type: string; +} + +/** Column returned by the `columns` resource. */ +export interface CIPColumn { + name: string; + dataType: string; + nullable: boolean; + ordinal: number; +} diff --git a/packages/b2c-grafana-datasource/src/datasource.ts b/packages/b2c-grafana-datasource/src/datasource.ts new file mode 100644 index 000000000..986493134 --- /dev/null +++ b/packages/b2c-grafana-datasource/src/datasource.ts @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import { DataSourceInstanceSettings, MetricFindValue, ScopedVars } from '@grafana/data'; +import { DataSourceWithBackend, getTemplateSrv } from '@grafana/runtime'; + +import { + B2CMetricsDataSourceOptions, + B2CMetricsQuery, + METRIC_CATEGORIES, + MetricCategory, + MetricOption, + PushDownFilter, +} from './types'; + +/** + * B2C Metrics datasource. + * + * Extends DataSourceWithBackend so query execution is delegated to the Go backend + * plugin (which owns OAuth, the Metrics API calls, tag enrichment, and data.Frame + * construction). The frontend uses `getResource(...)` to drive the dynamic, Prometheus- + * style query editor (metrics, label keys, label values) and ad-hoc filters. + */ +export class B2CMetricsDataSource extends DataSourceWithBackend { + constructor(instanceSettings: DataSourceInstanceSettings) { + super(instanceSettings); + } + + getDefaultQuery(): Partial { + return { category: 'overall', metricIds: [], labelFilters: [], groupBy: [] }; + } + + filterQuery(query: B2CMetricsQuery): boolean { + return Boolean(query.category); + } + + /** Interpolate template variables (incl. a dashboard $tenant and any filter values). */ + applyTemplateVariables(query: B2CMetricsQuery, scopedVars: ScopedVars): B2CMetricsQuery { + const t = getTemplateSrv(); + const rep = (v?: string) => (v ? t.replace(v, scopedVars) : v); + return { + ...query, + category: t.replace(query.category, scopedVars) as MetricCategory, + tenantId: rep(query.tenantId), + apiFamily: rep(query.apiFamily), + apiName: rep(query.apiName), + ocapiCategory: rep(query.ocapiCategory), + ocapiApi: rep(query.ocapiApi), + thirdPartyServiceId: rep(query.thirdPartyServiceId), + metricIds: query.metricIds?.map((m) => t.replace(m, scopedVars)), + labelFilters: query.labelFilters?.map((f) => ({ ...f, value: t.replace(f.value, scopedVars) })), + }; + } + + // ---- Discovery helpers (used by the query editor) -------------------------- + + /** Metric options (metricId + unit) for a category, probed live by the backend. */ + async getMetricOptions(category: string, tenantId?: string): Promise { + if (!category) { + return []; + } + return (await this.getResource('metrics', { category, tenantId: tenantId ?? '' })) as MetricOption[]; + } + + /** Server-side (push-down) filter descriptors for a category, with enum values. */ + async getPushDownFilters(category: string): Promise { + if (!category) { + return []; + } + return (await this.getResource('push-down-filters', { category })) as PushDownFilter[]; + } + + /** Derived (post-fetch) label keys for a category — for Label filters + Group by. */ + async getLabelKeys(category: string, tenantId?: string): Promise { + if (!category) { + return []; + } + return (await this.getResource('label-keys', { category, tenantId: tenantId ?? '' })) as string[]; + } + + /** Distinct values for a label key (server enum for push-down keys, else probed). */ + async getLabelValues(category: string, key: string, tenantId?: string): Promise { + if (!category || !key) { + return []; + } + return (await this.getResource('label-values', { category, key, tenantId: tenantId ?? '' })) as string[]; + } + + // ---- Template variables ---------------------------------------------------- + + /** + * Powers dashboard template variables. Supported queries: + * categories + * metrics() + * labelKeys() + * labelValues(,) + */ + async metricFindQuery(query: string): Promise { + const q = (query || '').trim(); + if (q === '' || q === 'categories') { + return METRIC_CATEGORIES.map((c) => ({ text: c, value: c })); + } + const m = q.match(/^(\w+)\s*\((.*)\)$/); + if (!m) { + return []; + } + const fn = m[1]; + const args = m[2].split(',').map((s) => s.trim()); + try { + if (fn === 'metrics') { + return (await this.getMetricOptions(args[0])).map((o) => ({ text: o.label, value: o.value })); + } + if (fn === 'labelKeys') { + return (await this.getLabelKeys(args[0])).map((k) => ({ text: k, value: k })); + } + if (fn === 'labelValues') { + return (await this.getLabelValues(args[0], args[1])).map((v) => ({ text: v, value: v })); + } + } catch (error) { + console.error('metricFindQuery error:', error); + } + return []; + } +} diff --git a/packages/b2c-grafana-datasource/src/img/logo.svg b/packages/b2c-grafana-datasource/src/img/logo.svg new file mode 100644 index 000000000..fefa1e9ca --- /dev/null +++ b/packages/b2c-grafana-datasource/src/img/logo.svg @@ -0,0 +1,7 @@ + + + + + + B2C + diff --git a/packages/b2c-grafana-datasource/src/module.ts b/packages/b2c-grafana-datasource/src/module.ts new file mode 100644 index 000000000..7846b494e --- /dev/null +++ b/packages/b2c-grafana-datasource/src/module.ts @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import { DataSourcePlugin } from '@grafana/data'; + +import { B2CMetricsDataSource } from './datasource'; +import { ConfigEditor } from './ConfigEditor'; +import { QueryEditor } from './QueryEditor'; +import { B2CMetricsDataSourceOptions, B2CMetricsQuery } from './types'; + +/** + * Grafana datasource plugin registration for B2C Metrics + */ +export const plugin = new DataSourcePlugin( + B2CMetricsDataSource +) + .setConfigEditor(ConfigEditor) + .setQueryEditor(QueryEditor); diff --git a/packages/b2c-grafana-datasource/src/plugin.json b/packages/b2c-grafana-datasource/src/plugin.json new file mode 100644 index 000000000..37f4fb713 --- /dev/null +++ b/packages/b2c-grafana-datasource/src/plugin.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://raw.githubusercontent.com/grafana/grafana/master/docs/sources/developers/plugins/plugin.schema.json", + "type": "datasource", + "name": "Salesforce B2C Commerce Metrics", + "id": "salesforce-b2c-metrics-datasource", + "backend": true, + "executable": "gpx_b2c_metrics", + "alerting": true, + "metrics": true, + "annotations": false, + "logs": false, + "tracing": false, + "info": { + "version": "0.1.0", + "description": "Datasource for Salesforce B2C Commerce Metrics API - monitor SCAPI, OCAPI, eCDN, MRT, and custom code performance", + "author": { + "name": "Salesforce", + "url": "https://developer.salesforce.com/docs/commerce/commerce-api" + }, + "keywords": [ + "salesforce", + "b2c commerce", + "metrics", + "observability", + "scapi", + "ocapi" + ], + "logos": { + "small": "img/logo.svg", + "large": "img/logo.svg" + }, + "links": [ + { + "name": "Documentation", + "url": "https://github.com/SalesforceCommerceCloud/b2c-developer-tooling" + } + ], + "screenshots": [], + "updated": "2026-07-14" + }, + "routes": [], + "dependencies": { + "grafanaDependency": ">=9.0.0", + "plugins": [] + } +} diff --git a/packages/b2c-grafana-datasource/src/types.ts b/packages/b2c-grafana-datasource/src/types.ts new file mode 100644 index 000000000..616bdc387 --- /dev/null +++ b/packages/b2c-grafana-datasource/src/types.ts @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import { DataQuery, DataSourceJsonData } from '@grafana/data'; + +/** + * Metrics API categories (9 endpoints) + */ +export const METRIC_CATEGORIES = [ + 'overall', + 'sales', + 'ecdn', + 'third-party', + 'scapi', + 'scapi-hooks', + 'mrt', + 'controller', + 'ocapi', +] as const; + +export type MetricCategory = (typeof METRIC_CATEGORIES)[number]; + +/** + * A post-fetch label filter on an enriched series tag (the "Label filters" tier). + * `key` is a derived dimension (metricId, statusClass, cacheStatus, host, ...); + * `op` is '=' or '!='. + */ +export interface LabelFilter { + key: string; + op: '=' | '!='; + value: string; +} + +/** + * Query model for B2C Metrics datasource. + * + * Filters live in two tiers that mirror the Metrics API: + * - **Push-down** (server) filters — sent to the API, validated against a fixed enum, + * and cause the API to drill down: `apiFamily`, `apiName`, `ocapiCategory`, + * `ocapiApi`, `thirdPartyServiceId`. + * - **Label filters** — applied post-fetch on enriched tags (`labelFilters`). + */ +export interface B2CMetricsQuery extends DataQuery { + /** Metrics category (determines which endpoint to call). */ + category: MetricCategory; + + /** Metrics to return (metricIds). Empty = all metrics in the category. Multi-select. */ + metricIds?: string[]; + + // Push-down (server) filters — validated dropdowns of the server enum in the editor. + apiFamily?: string; + apiName?: string; + ocapiCategory?: string; + ocapiApi?: string; + thirdPartyServiceId?: string; + + /** Post-fetch label filters (the "Label filters" tier). */ + labelFilters?: LabelFilter[]; + + /** Label keys to group by — drives the per-series legend/display name. */ + groupBy?: string[]; + + /** + * Optional per-query tenant override (empty = datasource default). Tenants on the + * same realm share the datasource's shortCode + OAuth client; a dashboard `$tenant` + * variable can switch e.g. prd ↔ stg. Template-interpolated before sending. + */ + tenantId?: string; +} + +/** A metric option returned by the `metrics` resource (value + label + unit). */ +export interface MetricOption { + value: string; + label: string; + unit: string; +} + +/** A push-down filter descriptor returned by the `push-down-filters` resource. */ +export interface PushDownFilter { + key: string; + hasEnum: boolean; + values?: string[]; +} + +/** + * DataSource configuration (stored in jsonData) + */ +export interface B2CMetricsDataSourceOptions extends DataSourceJsonData { + /** + * Instance short code (e.g., 'zzpq_013') + */ + shortCode: string; + + /** + * Tenant ID (normalized form or f_ecom_ prefixed) + */ + tenantId: string; + + /** + * OAuth client ID + */ + clientId: string; + + /** + * Account Manager host (optional, defaults to SDK value) + */ + accountManagerHost?: string; + + /** + * Full Metrics API base URL override (optional) + * Example: http://mock-metrics:8080/observability/metrics/v1 + * Empty → derives https://{shortCode}.api.commercecloud.salesforce.com/observability/metrics/v1 + */ + apiUrl?: string; + + /** + * Full OAuth token endpoint URL override (optional) + * Example: http://mock-metrics:8080/dwsso/oauth2/access_token + * Empty → derives https://{accountManagerHost}/dwsso/oauth2/access_token + */ + tokenUrl?: string; +} + +/** + * Secure configuration (stored encrypted in secureJsonData) + */ +export interface B2CMetricsSecureJsonData { + /** + * OAuth client secret + */ + clientSecret?: string; +} diff --git a/packages/b2c-grafana-datasource/test-endpoints.sh b/packages/b2c-grafana-datasource/test-endpoints.sh new file mode 100755 index 000000000..334a8cf3f --- /dev/null +++ b/packages/b2c-grafana-datasource/test-endpoints.sh @@ -0,0 +1,192 @@ +#!/bin/bash +# End-to-end verification script for B2C Metrics Grafana plugin +# Run after: docker compose up --build + +set -e + +GRAFANA_URL="http://localhost:3000" +MOCK_URL="http://localhost:8080" +DATASOURCE_UID="b2c-metrics-demo" +PLUGIN_ID="salesforce-b2c-metrics-datasource" + +echo "=== B2C Metrics Grafana Plugin — Runtime Verification ===" +echo "" + +# Color codes +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +pass() { + echo -e "${GREEN}✓${NC} $1" +} + +fail() { + echo -e "${RED}✗${NC} $1" + exit 1 +} + +warn() { + echo -e "${YELLOW}⚠${NC} $1" +} + +# Wait for Grafana health +echo "1. Waiting for Grafana to be healthy..." +TIMEOUT=90 +ELAPSED=0 +while [ $ELAPSED -lt $TIMEOUT ]; do + if curl -s "$GRAFANA_URL/api/health" | grep -q '"database":"ok"'; then + pass "Grafana is healthy (${ELAPSED}s)" + break + fi + sleep 2 + ELAPSED=$((ELAPSED + 2)) + if [ $ELAPSED -ge $TIMEOUT ]; then + fail "Grafana health check timeout after ${TIMEOUT}s" + fi +done +echo "" + +# Check mock service +echo "2. Verifying mock-metrics service..." +if curl -s "$MOCK_URL/dwsso/oauth2/access_token" -X POST | grep -q '"access_token"'; then + pass "Mock OAuth endpoint responding" +else + fail "Mock OAuth endpoint not responding" +fi + +ORG_ID="bdpx_org_123" +CATEGORY="overall" +FROM=$(date -u -v-6H +%s) # 6 hours ago (macOS date format) +TO=$(date -u +%s) +if curl -s "$MOCK_URL/observability/metrics/v1/organizations/$ORG_ID/metrics/$CATEGORY?from=$FROM&to=$TO" | grep -q '"metricId"'; then + pass "Mock Metrics endpoint responding" +else + fail "Mock Metrics endpoint not responding" +fi +echo "" + +# Check plugin loaded +echo "3. Verifying plugin loaded..." +PLUGIN_RESPONSE=$(curl -s "$GRAFANA_URL/api/plugins/$PLUGIN_ID/settings") +if echo "$PLUGIN_RESPONSE" | grep -q '"enabled":true'; then + pass "Plugin enabled" +else + fail "Plugin not enabled" +fi + +if echo "$PLUGIN_RESPONSE" | grep -q '"backend":true'; then + pass "Backend registered" +else + fail "Backend not registered" +fi +echo "" + +# Check datasource provisioned +echo "4. Verifying datasource provisioned..." +DS_RESPONSE=$(curl -s "$GRAFANA_URL/api/datasources") +if echo "$DS_RESPONSE" | grep -q "\"uid\":\"$DATASOURCE_UID\""; then + pass "Datasource provisioned (uid: $DATASOURCE_UID)" +else + fail "Datasource not found (uid: $DATASOURCE_UID)" +fi + +DS_NAME=$(echo "$DS_RESPONSE" | grep -A 10 "\"uid\":\"$DATASOURCE_UID\"" | grep '"name"' | head -1 | cut -d'"' -f4) +if [ -n "$DS_NAME" ]; then + pass "Datasource name: $DS_NAME" +fi +echo "" + +# Check datasource health +echo "5. Testing datasource health check..." +HEALTH_RESPONSE=$(curl -s "$GRAFANA_URL/api/datasources/uid/$DATASOURCE_UID/health") +if echo "$HEALTH_RESPONSE" | grep -q '"status":"OK"'; then + pass "Datasource health check passed" + HEALTH_MSG=$(echo "$HEALTH_RESPONSE" | grep -o '"message":"[^"]*"' | cut -d'"' -f4) + if [ -n "$HEALTH_MSG" ]; then + echo " Message: $HEALTH_MSG" + fi +else + fail "Datasource health check failed" + echo " Response: $HEALTH_RESPONSE" +fi +echo "" + +# Test query execution +echo "6. Executing test query (SCAPI metrics)..." +QUERY_PAYLOAD=$(cat < /dev/null; then + FIRST_VALUE=$(echo "$QUERY_RESPONSE" | jq -r '.results.A.frames[0].data.values[1][0]' 2>/dev/null || echo "") + if [ -n "$FIRST_VALUE" ] && [ "$FIRST_VALUE" != "null" ]; then + echo " Sample value: $FIRST_VALUE" + fi + fi + else + warn "No data points returned (may need longer time range)" + fi +else + fail "Query did not return frames" + echo " Response: $QUERY_RESPONSE" +fi +echo "" + +# Summary +echo "=== VERIFICATION COMPLETE ===" +echo "" +pass "All checks passed" +echo "" +echo "Demo dashboard available at:" +echo " $GRAFANA_URL/d/b2c-metrics-demo/b2c-commerce-metrics-demo" +echo "" +echo "To stop the demo:" +echo " docker compose down -v" +echo "" diff --git a/packages/b2c-grafana-datasource/tsconfig.json b/packages/b2c-grafana-datasource/tsconfig.json new file mode 100644 index 000000000..c43577ff7 --- /dev/null +++ b/packages/b2c-grafana-datasource/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM"], + "jsx": "react-jsx", + "moduleResolution": "node", + "esModuleInterop": true, + "skipLibCheck": true, + "strict": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "types": ["node"], + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "declaration": false, + "sourceMap": true + }, + "include": ["src/**/*", ".config/**/*"], + "exclude": ["node_modules", "dist", "pkg"] +} diff --git a/packages/b2c-tooling-sdk-go/API.md b/packages/b2c-tooling-sdk-go/API.md new file mode 100644 index 000000000..1c77e81a2 --- /dev/null +++ b/packages/b2c-tooling-sdk-go/API.md @@ -0,0 +1,310 @@ +# B2C Tooling SDK for Go - API Reference + +## Package Structure + +``` +github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go +├── auth/ # OAuth2 authentication +├── clients/ # Tenant ID helpers +│ └── metrics/ # Metrics API client +└── operations/metrics/ # High-level metrics operations +``` + +## auth + +### Constants + +```go +const DefaultAccountManagerHost = "account.demandware.com" +const ClientIDHeader = "x-dw-client-id" +``` + +### Types + +```go +type OAuthConfig struct { + ClientID string + ClientSecret string + AccountManagerHost string // Optional, defaults to DefaultAccountManagerHost + Scopes []string // Optional OAuth scopes +} +``` + +### Functions + +```go +// NewOAuthStrategy creates a new OAuth2 authentication strategy using client credentials flow. +// Returns a strategy that provides an http.Client with automatic token management and header injection. +func NewOAuthStrategy(cfg OAuthConfig) *OAuthStrategy + +// Client returns the http.Client configured with OAuth authentication. +func (s *OAuthStrategy) Client() *http.Client + +// WithAdditionalScopes returns a new OAuthStrategy with additional scopes merged with existing scopes. +// Useful for creating scoped clients from a base strategy. +func (s *OAuthStrategy) WithAdditionalScopes(additionalScopes []string) *OAuthStrategy + +// FormatScopes joins scopes into a space-delimited string for OAuth token requests. +func FormatScopes(scopes []string) string +``` + +## clients + +### Constants + +```go +const OrganizationIDPrefix = "f_ecom_" +const ScapiTenantScopePrefix = "SALESFORCE_COMMERCE_API:" +``` + +### Functions + +```go +// NormalizeTenantID normalizes a tenant ID by: +// 1. Trimming whitespace +// 2. Taking substring before first dot (if present) +// 3. Stripping leading "f_ecom_" prefix (if present) +// 4. Replacing all hyphens with underscores +// +// Examples: +// "f_ecom_bdpx_prd" → "bdpx_prd" +// "abcd-123.dx.example.com" → "abcd_123" +func NormalizeTenantID(value string) string + +// ToOrganizationID ensures a tenant ID has the required f_ecom_ prefix for SCAPI organizationId. +// +// Example: "bdpx_prd" → "f_ecom_bdpx_prd" +func ToOrganizationID(tenantID string) string + +// BuildTenantScope constructs the tenant-specific SCAPI OAuth scope. +// +// Example: "bdpx_prd" → "SALESFORCE_COMMERCE_API:bdpx_prd" +func BuildTenantScope(tenantID string) string +``` + +## clients/metrics + +### Constants + +```go +const MetricsScope = "sfcc.metrics" +``` + +### Types + +```go +type DataPoint struct { + Timestamp int64 `json:"timestamp"` // Epoch milliseconds (normalized from API's seconds) + Value float64 `json:"value"` +} + +type DataSeries struct { + ID string `json:"id"` + Name string `json:"name"` + Data []DataPoint `json:"data"` + Tags metricsops.MetricSeriesTags `json:"tags,omitempty"` // Enriched structured tags +} + +type Metric struct { + MetricID string `json:"metricId"` + Title string `json:"title"` + Description string `json:"description"` + Unit string `json:"unit,omitempty"` + DataSeries []DataSeries `json:"dataSeries"` +} + +type MetricsDataResponse struct { + Data []Metric `json:"data"` +} + +type Config struct { + ShortCode string // SCAPI instance short code (e.g., "kv7kzm78") + TenantID string // Tenant ID (with or without f_ecom_ prefix) +} +``` + +### Functions + +```go +// NewClient creates a new Metrics API client. +// Automatically handles OAuth scopes, timestamp normalization, and tag enrichment. +func NewClient(cfg Config, authStrategy *auth.OAuthStrategy) *Client + +// GetOverallMetrics retrieves overall application metrics. +func (c *Client) GetOverallMetrics(ctx context.Context, from, to time.Time, filters map[string]string) (*MetricsDataResponse, error) + +// GetSalesMetrics retrieves sales metrics. +func (c *Client) GetSalesMetrics(ctx context.Context, from, to time.Time, filters map[string]string) (*MetricsDataResponse, error) + +// GetEcdnMetrics retrieves eCDN metrics. +func (c *Client) GetEcdnMetrics(ctx context.Context, from, to time.Time, filters map[string]string) (*MetricsDataResponse, error) + +// GetThirdPartyMetrics retrieves third-party service metrics. +// Filters: "thirdPartyServiceId" +func (c *Client) GetThirdPartyMetrics(ctx context.Context, from, to time.Time, filters map[string]string) (*MetricsDataResponse, error) + +// GetScapiMetrics retrieves SCAPI metrics. +// Filters: "apiFamily", "apiName" +func (c *Client) GetScapiMetrics(ctx context.Context, from, to time.Time, filters map[string]string) (*MetricsDataResponse, error) + +// GetScapiHooksMetrics retrieves SCAPI hooks metrics. +func (c *Client) GetScapiHooksMetrics(ctx context.Context, from, to time.Time, filters map[string]string) (*MetricsDataResponse, error) + +// GetMrtMetrics retrieves MRT (Managed Runtime) metrics. +func (c *Client) GetMrtMetrics(ctx context.Context, from, to time.Time, filters map[string]string) (*MetricsDataResponse, error) + +// GetControllerMetrics retrieves controller metrics. +func (c *Client) GetControllerMetrics(ctx context.Context, from, to time.Time, filters map[string]string) (*MetricsDataResponse, error) + +// GetOcapiMetrics retrieves OCAPI metrics. +// Filters: "ocapiCategory", "ocapiApi" +func (c *Client) GetOcapiMetrics(ctx context.Context, from, to time.Time, filters map[string]string) (*MetricsDataResponse, error) +``` + +## operations/metrics + +### Constants + +```go +const MetricsRetentionPeriod = 30 * 24 * time.Hour // 30 days +const MetricsDefaultWindow = 24 * time.Hour // 24 hours +const MetricsRetentionSafetyMargin = 5 * time.Minute // 5 minutes +``` + +### Types + +```go +type MetricSeriesTags map[string]string + +// Always contains "realm" and optionally "environment". +// Category-specific keys: apiFamily, host, cacheStatus, statusClass, +// ocapiCategory, controller, exceptionType, aggregation. + +type MetricsTagContext struct { + TenantID string + APIFamily string // Optional SCAPI filter + APIName string // Optional SCAPI filter + OcapiCategory string // Optional OCAPI filter + OcapiAPI string // Optional OCAPI filter + ThirdPartyServiceID string // Optional third-party filter +} + +type ParseSeriesTagsParams struct { + Category string + MetricID string + SeriesID string + Context MetricsTagContext +} + +type MetricsWindowInput struct { + From interface{} // time.Time, int64 (epoch ms), or string (relative/ISO) + To interface{} // time.Time, int64 (epoch ms), or string (relative/ISO) + Window interface{} // time.Duration or string (relative) +} + +type ResolvedMetricsWindow struct { + From time.Time + To time.Time + FromEpochSeconds int64 + ToEpochSeconds int64 + ClampedFrom bool // True if from was clamped to stay within retention + DefaultedWindow bool // True if a bound was derived from the 24-hour default +} +``` + +### Functions + +```go +// ParseSeriesTags extracts structured dimension tags from a series ID. +// Combines three tiers: +// 1. Request identity (realm/environment from tenant ID) +// 2. String heuristics (category/metric-specific parsing) +// 3. Applied filters (override heuristics) +func ParseSeriesTags(params ParseSeriesTagsParams) MetricSeriesTags + +// ParseMetricsBound parses a single time bound into time.Time. +// Accepts: time.Time, int64 (epoch ms), or string (relative like "5m" or ISO 8601). +func ParseMetricsBound(value interface{}, now time.Time) (time.Time, error) + +// ResolveMetricsWindow resolves from/to/window inputs into concrete bounds. +// Always produces explicit from+to. Enforces 30-day retention and 24-hour default window. +func ResolveMetricsWindow(input MetricsWindowInput, now time.Time) (*ResolvedMetricsWindow, error) +``` + +## Complete Usage Example + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go/auth" + "github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go/clients/metrics" + metricsops "github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go/operations/metrics" +) + +func main() { + // 1. Create OAuth strategy + authClient := auth.NewOAuthStrategy(auth.OAuthConfig{ + ClientID: "your-client-id", + ClientSecret: "your-client-secret", + }) + + // 2. Create metrics client (scopes handled automatically) + client := metrics.NewClient(metrics.Config{ + ShortCode: "kv7kzm78", + TenantID: "bdpx_prd", + }, authClient) + + // 3. Resolve time window (with retention enforcement) + window, err := metricsops.ResolveMetricsWindow(metricsops.MetricsWindowInput{ + From: "24h", // 24 hours ago + Window: "1h", // 1 hour window + }, time.Now()) + if err != nil { + panic(err) + } + + // 4. Fetch metrics + ctx := context.Background() + resp, err := client.GetScapiMetrics(ctx, window.From, window.To, map[string]string{ + "apiFamily": "product", + }) + if err != nil { + panic(err) + } + + // 5. Process enriched data + for _, metric := range resp.Data { + fmt.Printf("Metric: %s (%s)\n", metric.Title, metric.MetricID) + for _, series := range metric.DataSeries { + // Tags are automatically parsed and enriched + fmt.Printf(" Series: %s\n", series.Name) + fmt.Printf(" Tags: realm=%s, environment=%s, apiFamily=%s\n", + series.Tags["realm"], + series.Tags["environment"], + series.Tags["apiFamily"], + ) + for _, point := range series.Data { + t := time.UnixMilli(point.Timestamp) // Already normalized to ms + fmt.Printf(" %s: %.2f %s\n", t.Format(time.RFC3339), point.Value, metric.Unit) + } + } + } +} +``` + +## Parity with TypeScript SDK + +This Go SDK maintains exact behavioral parity with the TypeScript SDK: + +- **Tag extraction**: Both SDKs assert against the same golden test fixture (`metrics-tags.golden.json`) +- **Window resolution**: Identical 30-day retention and 24-hour default window rules +- **Timestamp normalization**: API returns epoch seconds, both SDKs normalize to milliseconds +- **OAuth scopes**: Same automatic scope construction (sfcc.metrics + tenant-specific scope) +- **Tenant ID normalization**: Same string processing rules + +The golden test ensures zero drift between implementations. diff --git a/packages/b2c-tooling-sdk-go/GRAFANA_INTEGRATION.md b/packages/b2c-tooling-sdk-go/GRAFANA_INTEGRATION.md new file mode 100644 index 000000000..00f353627 --- /dev/null +++ b/packages/b2c-tooling-sdk-go/GRAFANA_INTEGRATION.md @@ -0,0 +1,536 @@ +# Grafana Plugin Integration Guide + +This document describes how to integrate the Go B2C Tooling SDK into a Grafana backend datasource plugin for the Metrics API. + +## Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Grafana Frontend (React) │ +│ ├── ConfigEditor (datasource settings) │ +│ ├── QueryEditor (query builder UI) │ +│ └── DataSourceApi (query execution bridge) │ +└───────────────────────────┬─────────────────────────────────┘ + │ JSON-RPC over HTTP +┌───────────────────────────┴─────────────────────────────────┐ +│ Grafana Backend Plugin (Go) │ +│ ├── plugin.json (metadata) │ +│ ├── datasource.go (QueryDataHandler, CheckHealthHandler) │ +│ └── B2C Tooling SDK │ +│ ├── auth.OAuthStrategy (client credentials) │ +│ ├── metrics.Client (category endpoints) │ +│ └── operations/metrics (window + tags) │ +└───────────────────────────┬─────────────────────────────────┘ + │ HTTPS +┌───────────────────────────┴─────────────────────────────────┐ +│ Metrics API │ +│ https://{shortCode}.api.commercecloud.salesforce.com/ │ +│ observability/metrics/v1 │ +└───────────────────────────────────────────────────────────────┘ +``` + +## Plugin Configuration + +### plugin.json + +```json +{ + "type": "datasource", + "name": "B2C Commerce Metrics", + "id": "salesforce-b2c-metrics", + "backend": true, + "executable": "gpx_b2c_metrics", + "alerting": true, + "metrics": true, + "routes": [], + "info": { + "version": "0.1.0", + "description": "B2C Commerce Metrics API datasource", + "author": { "name": "Salesforce" }, + "logos": { + "small": "img/logo.svg", + "large": "img/logo.svg" + } + } +} +``` + +### Datasource Settings + +#### jsonData (non-sensitive) + +```json +{ + "shortCode": "kv7kzm78", + "tenantId": "bdpx_prd", + "accountManagerHost": "account.demandware.com" // Optional +} +``` + +#### secureJsonData (encrypted server-side) + +```json +{ + "clientId": "your-client-id", + "clientSecret": "your-client-secret" +} +``` + +Accessed in Go via: +```go +settings.DecryptedSecureJSONData["clientId"] +settings.DecryptedSecureJSONData["clientSecret"] +``` + +## Backend Implementation + +### Main Plugin Registration + +```go +package main + +import ( + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" +) + +func main() { + if err := datasource.Manage( + "salesforce-b2c-metrics", + newDatasource, + datasource.ManageOpts{}, + ); err != nil { + log.DefaultLogger.Error(err.Error()) + } +} + +func newDatasource(ctx context.Context, settings backend.DataSourceInstanceSettings) (backend.DataSource, error) { + return &B2CMetricsDatasource{ + settings: settings, + }, nil +} +``` + +### Datasource Handler + +```go +package main + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data" + + "github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go/auth" + "github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go/clients/metrics" + metricsops "github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go/operations/metrics" +) + +type B2CMetricsDatasource struct { + settings backend.DataSourceInstanceSettings +} + +// QueryModel represents the query submitted from the frontend. +type QueryModel struct { + RefID string `json:"refId"` + Category string `json:"category"` // "overall", "scapi", "ocapi", etc. + APIFamily string `json:"apiFamily"` // SCAPI filter + APIName string `json:"apiName"` // SCAPI filter + OcapiCategory string `json:"ocapiCategory"` // OCAPI filter + OcapiAPI string `json:"ocapiApi"` // OCAPI filter + ThirdPartyID string `json:"thirdPartyServiceId"` // Third-party filter +} + +func (d *B2CMetricsDatasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + response := backend.NewQueryDataResponse() + + // Parse datasource settings + var jsonData struct { + ShortCode string `json:"shortCode"` + TenantID string `json:"tenantId"` + AccountManagerHost string `json:"accountManagerHost"` + } + if err := json.Unmarshal(req.PluginContext.DataSourceInstanceSettings.JSONData, &jsonData); err != nil { + return response, fmt.Errorf("failed to parse datasource settings: %w", err) + } + + // Get credentials from secure storage + clientID := req.PluginContext.DataSourceInstanceSettings.DecryptedSecureJSONData["clientId"] + clientSecret := req.PluginContext.DataSourceInstanceSettings.DecryptedSecureJSONData["clientSecret"] + if clientID == "" || clientSecret == "" { + return response, fmt.Errorf("missing client credentials") + } + + // Create OAuth strategy + authCfg := auth.OAuthConfig{ + ClientID: clientID, + ClientSecret: clientSecret, + AccountManagerHost: jsonData.AccountManagerHost, + } + if authCfg.AccountManagerHost == "" { + authCfg.AccountManagerHost = auth.DefaultAccountManagerHost + } + authStrategy := auth.NewOAuthStrategy(authCfg) + + // Create metrics client + client := metrics.NewClient(metrics.Config{ + ShortCode: jsonData.ShortCode, + TenantID: jsonData.TenantID, + }, authStrategy) + + // Process each query + for _, query := range req.Queries { + var qm QueryModel + if err := json.Unmarshal(query.JSON, &qm); err != nil { + response.Responses[query.RefID] = backend.ErrDataResponse(backend.StatusBadRequest, fmt.Sprintf("failed to parse query: %v", err)) + continue + } + + // Resolve time window (enforce retention) + window, err := metricsops.ResolveMetricsWindow(metricsops.MetricsWindowInput{ + From: query.TimeRange.From.UnixMilli(), + To: query.TimeRange.To.UnixMilli(), + }, time.Now()) + if err != nil { + response.Responses[qm.RefID] = backend.ErrDataResponse(backend.StatusBadRequest, fmt.Sprintf("invalid time window: %v", err)) + continue + } + + // Build category filters + filters := map[string]string{ + "apiFamily": qm.APIFamily, + "apiName": qm.APIName, + "ocapiCategory": qm.OcapiCategory, + "ocapiApi": qm.OcapiAPI, + "thirdPartyServiceId": qm.ThirdPartyID, + } + + // Fetch metrics for the category + var data *metrics.MetricsDataResponse + switch qm.Category { + case "overall": + data, err = client.GetOverallMetrics(ctx, window.From, window.To, filters) + case "scapi": + data, err = client.GetScapiMetrics(ctx, window.From, window.To, filters) + case "ocapi": + data, err = client.GetOcapiMetrics(ctx, window.From, window.To, filters) + case "third-party": + data, err = client.GetThirdPartyMetrics(ctx, window.From, window.To, filters) + case "ecdn": + data, err = client.GetEcdnMetrics(ctx, window.From, window.To, filters) + default: + err = fmt.Errorf("unknown category: %s", qm.Category) + } + + if err != nil { + response.Responses[qm.RefID] = backend.ErrDataResponse(backend.StatusInternal, fmt.Sprintf("metrics API error: %v", err)) + continue + } + + // Convert to Grafana frames (one time field + one value field per series) + frames := convertToFrames(data, qm, window) + response.Responses[qm.RefID] = backend.DataResponse{Frames: frames} + } + + return response, nil +} + +// convertToFrames converts Metrics API response to Grafana data frames. +// Each series becomes a separate frame with: +// - One time field (timestamps) +// - One value field (data points) +// - Labels from enriched tags +func convertToFrames(data *metrics.MetricsDataResponse, qm QueryModel, window *metricsops.ResolvedMetricsWindow) data.Frames { + var frames data.Frames + + for _, metric := range data.Data { + for _, series := range metric.DataSeries { + // Extract timestamps and values + timeVals := make([]time.Time, len(series.Data)) + valueVals := make([]float64, len(series.Data)) + for i, point := range series.Data { + timeVals[i] = time.UnixMilli(point.Timestamp) + valueVals[i] = point.Value + } + + // Build frame name from tags + frameName := buildFrameName(metric.Title, series.Tags) + + // Create frame with time + value fields + frame := data.NewFrame(frameName, + data.NewField("time", nil, timeVals), + data.NewField(metric.MetricID, data.Labels(series.Tags), valueVals), + ) + + // Set field config (unit, display name) + if metric.Unit != "" { + frame.Fields[1].Config = &data.FieldConfig{ + Unit: metric.Unit, + } + } + + // Add metadata + frame.Meta = &data.FrameMeta{ + ExecutedQueryString: fmt.Sprintf("category=%s from=%s to=%s", qm.Category, window.FromIso, window.ToIso), + } + + if window.ClampedFrom { + frame.Meta.Notices = append(frame.Meta.Notices, data.Notice{ + Severity: data.NoticeSeverityWarning, + Text: "Time range was clamped to 30-day retention window", + }) + } + + frames = append(frames, frame) + } + } + + return frames +} + +// buildFrameName constructs a human-readable frame name from tags. +func buildFrameName(metricTitle string, tags map[string]string) string { + // Example: "SCAPI Total Calls (product, shopper-products, bdpx/prd)" + parts := []string{metricTitle} + + if apiFamily, ok := tags["apiFamily"]; ok { + parts = append(parts, apiFamily) + } + if apiName, ok := tags["apiName"]; ok { + parts = append(parts, apiName) + } + if host, ok := tags["host"]; ok { + parts = append(parts, host) + } + + realm := tags["realm"] + env := tags["environment"] + if env != "" { + parts = append(parts, fmt.Sprintf("%s/%s", realm, env)) + } else if realm != "" { + parts = append(parts, realm) + } + + return fmt.Sprintf("%s (%s)", parts[0], strings.Join(parts[1:], ", ")) +} + +func (d *B2CMetricsDatasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + // Parse settings and test OAuth + API connectivity + // (Implementation similar to QueryData setup, but with a simple test request) + return &backend.CheckHealthResult{ + Status: backend.HealthStatusOk, + Message: "Successfully connected to Metrics API", + }, nil +} +``` + +## Frontend Integration + +### QueryEditor.tsx + +```tsx +import React from 'react'; +import { QueryEditorProps, SelectableValue } from '@grafana/data'; +import { InlineField, Select, Input } from '@grafana/ui'; + +const CATEGORIES: Array> = [ + { label: 'Overall', value: 'overall' }, + { label: 'SCAPI', value: 'scapi' }, + { label: 'OCAPI', value: 'ocapi' }, + { label: 'Third-party', value: 'third-party' }, + { label: 'eCDN', value: 'ecdn' }, + { label: 'MRT', value: 'mrt' }, + { label: 'Controller', value: 'controller' }, + { label: 'SCAPI Hooks', value: 'scapi-hooks' }, + { label: 'Sales', value: 'sales' }, +]; + +export function QueryEditor(props: QueryEditorProps) { + const { query, onChange, onRunQuery } = props; + + return ( +
+ + onChange({ ...query, apiFamily: e.currentTarget.value })} + onBlur={onRunQuery} + placeholder="product, checkout, etc." + /> + + + onChange({ ...query, apiName: e.currentTarget.value })} + onBlur={onRunQuery} + placeholder="shopper-products, etc." + /> + + + )} + + {query.category === 'ocapi' && ( + <> + + onChange({ ...query, ocapiCategory: e.currentTarget.value })} + onBlur={onRunQuery} + placeholder="shop, data" + /> + + + onChange({ ...query, ocapiApi: e.currentTarget.value })} + onBlur={onRunQuery} + /> + + + )} + + {query.category === 'third-party' && ( + + onChange({ ...query, thirdPartyServiceId: e.currentTarget.value })} + onBlur={onRunQuery} + /> + + )} +
+ ); +} +``` + +## Key Integration Points + +### 1. OAuth Token Management + +The SDK handles all token lifecycle: +- Initial fetch via client credentials +- Automatic refresh on expiry +- Single-flight token requests (no thundering herd) +- Header injection (Authorization + x-dw-client-id) + +```go +authStrategy := auth.NewOAuthStrategy(auth.OAuthConfig{ + ClientID: clientID, + ClientSecret: clientSecret, +}) +// authStrategy.Client() returns http.Client ready to use +``` + +### 2. Time Window Resolution + +Grafana passes `query.TimeRange.From/To`. The SDK enforces retention: + +```go +window, err := metricsops.ResolveMetricsWindow(metricsops.MetricsWindowInput{ + From: query.TimeRange.From.UnixMilli(), + To: query.TimeRange.To.UnixMilli(), +}, time.Now()) + +// window.ClampedFrom indicates if from was moved forward into retention +// window.FromEpochSeconds / ToEpochSeconds are ready for API query params +``` + +### 3. Timestamp Normalization + +The Metrics API returns epoch **seconds**. The SDK normalizes to **milliseconds**: + +```go +for _, point := range series.Data { + t := time.UnixMilli(point.Timestamp) // Already ×1000 +} +``` + +### 4. Tag Enrichment + +Every series gets structured tags automatically: + +```go +for _, series := range metric.DataSeries { + // series.Tags is map[string]string with realm, environment, apiFamily, etc. + labels := data.Labels(series.Tags) // Convert to Grafana labels +} +``` + +## Testing Strategy + +### Unit Tests + +Already provided in the SDK: +- `auth/oauth_test.go` - OAuth flow with mock token endpoint +- `clients/tenant_test.go` - Tenant ID normalization +- `operations/metrics/tags_golden_test.go` - Tag extraction (32 golden cases) +- `operations/metrics/window_test.go` - Window resolution with retention + +### Integration Test (Plugin) + +```go +func TestDatasourceQueryData(t *testing.T) { + // Use httptest to mock Metrics API + apiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Return mock MetricsDataResponse JSON + w.Write([]byte(`{...}`)) + })) + defer apiServer.Close() + + // Create datasource with test settings pointing to mock server + // Execute QueryData + // Assert frames match expected structure +} +``` + +## Deployment + +1. Build the plugin: + ```bash + mage -v + ``` + +2. Sign the plugin (for distribution): + ```bash + npx @grafana/sign-plugin@latest + ``` + +3. Install in Grafana: + - Copy `dist/` to Grafana plugins directory + - Restart Grafana + - Add datasource via UI with credentials + +## Security Notes + +- **Client credentials** are stored in Grafana's encrypted secure JSON data (never in plain jsonData) +- **OAuth tokens** are cached in-memory by the SDK (not persisted) +- **TLS** is enforced for all Account Manager and Metrics API calls +- **Scopes** are automatically constructed (sfcc.metrics + tenant-specific) + +## Performance + +- **Token caching**: Reduces token endpoint load (1 token per scope set, reused until expiry) +- **HTTP connection pooling**: Provided by Go's default http.Client +- **Parallel queries**: Grafana executes multiple QueryData calls concurrently (SDK is safe for concurrent use) + +## Parity Guarantee + +The Go SDK's golden test ensures exact parity with the TypeScript SDK for tag extraction. Both implementations assert against the same `metrics-tags.golden.json` fixture, preventing drift. diff --git a/packages/b2c-tooling-sdk-go/README.md b/packages/b2c-tooling-sdk-go/README.md new file mode 100644 index 000000000..a92ef1182 --- /dev/null +++ b/packages/b2c-tooling-sdk-go/README.md @@ -0,0 +1,435 @@ +# B2C Tooling SDK for Go + +Go SDK for Salesforce B2C Commerce APIs — provides typed clients for the **Metrics API** and **CIP analytics warehouse**, with OAuth2 authentication, tenant ID normalization, and structured tag extraction. + +This SDK is **standalone** (no Grafana dependencies) and can be used in any Go application. It powers the [b2c-grafana-datasource](../b2c-grafana-datasource/) plugins but is designed for general-purpose use. + +## Features + +- **Metrics API Client**: 9 category endpoints (overall, scapi, ecdn, mrt, etc.) +- **CIP Client**: JDBC-over-Avatica connection for SQL queries +- **OAuth2 Authentication**: Client credentials flow with token caching and auto-refresh +- **Tag Enrichment**: Automatic extraction of structured labels from series IDs +- **Time Window Resolution**: 30-day retention enforcement, flexible input formats +- **Tenant ID Helpers**: Normalization and scope construction +- **Error Handling**: Typed errors with context +- **Testing**: 100% test coverage with golden test fixtures + +## Installation + +```bash +go get github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go +``` + +**Module path**: `github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go` + +**Go version**: 1.26+ + +## Quick Start + +### Metrics API Example + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go/auth" + "github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go/clients/metrics" +) + +func main() { + // 1. Create OAuth strategy + authClient := auth.NewOAuthStrategy(auth.OAuthConfig{ + ClientID: "your-client-id", + ClientSecret: "your-client-secret", + AccountManagerHost: auth.DefaultAccountManagerHost, // "account.demandware.com" + }) + + // 2. Create Metrics client + client := metrics.NewClient(metrics.Config{ + ShortCode: "kv7kzm78", + TenantID: "bdpx_prd", + }, authClient) + + // 3. Fetch overall metrics for last 24 hours + ctx := context.Background() + from := time.Now().Add(-24 * time.Hour) + to := time.Now() + + resp, err := client.GetOverallMetrics(ctx, from, to, nil) + if err != nil { + panic(err) + } + + // 4. Access metrics data + fmt.Printf("Fetched %d metrics\n", len(resp.Data)) + for _, metric := range resp.Data { + fmt.Printf("Metric: %s (%s)\n", metric.Title, metric.ID) + for _, series := range metric.DataSeries { + // Tags are automatically enriched (realm, environment, etc.) + fmt.Printf(" Series: %s, Tags: %+v\n", series.ID, series.Tags) + fmt.Printf(" Data points: %d\n", len(series.Data)) + } + } +} +``` + +### CIP SQL Query Example + +```go +package main + +import ( + "context" + "fmt" + + "github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go/auth" + "github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go/clients/cip" +) + +func main() { + // 1. Create OAuth strategy + authClient := auth.NewOAuthStrategy(auth.OAuthConfig{ + ClientID: "your-client-id", + ClientSecret: "your-client-secret", + }) + + // 2. Create CIP client + client, err := cip.NewClient(cip.Config{ + Host: "https://cip-us.commercecloud.salesforce.com", + TenantID: "bdpx_prd", + }, authClient) + if err != nil { + panic(err) + } + defer client.Close() + + // 3. Execute SQL query + ctx := context.Background() + query := ` + SELECT site_id, COUNT(*) as order_count + FROM orders + WHERE submit_date >= CURRENT_DATE - INTERVAL '7' DAY + GROUP BY site_id + ORDER BY order_count DESC + LIMIT 10 + ` + + rows, err := client.Query(ctx, query) + if err != nil { + panic(err) + } + defer rows.Close() + + // 4. Process results + for rows.Next() { + var siteID string + var orderCount int64 + if err := rows.Scan(&siteID, &orderCount); err != nil { + panic(err) + } + fmt.Printf("%s: %d orders\n", siteID, orderCount) + } +} +``` + +## Package Overview + +### `auth` + +OAuth2 client credentials flow with token caching. + +**Key Types**: +- `OAuthStrategy`: Main authentication client +- `OAuthConfig`: Configuration (client ID, secret, host) + +**Features**: +- Token caching by scope (in-memory) +- Auto-refresh on expiry (401 responses) +- x-dw-client-id header injection +- Configurable Account Manager host + +**Example**: +```go +authClient := auth.NewOAuthStrategy(auth.OAuthConfig{ + ClientID: "your-client-id", + ClientSecret: "your-client-secret", +}) +``` + +### `clients/metrics` + +Typed client for the Metrics API. + +**Key Types**: +- `Client`: Metrics API client +- `Config`: Configuration (shortCode, tenantID) +- `MetricsResponse`: Structured API response + +**Methods** (9 category endpoints): +- `GetOverallMetrics(ctx, from, to, filters)` +- `GetSalesMetrics(ctx, from, to, filters)` +- `GetECDNMetrics(ctx, from, to, filters)` +- `GetThirdPartyMetrics(ctx, from, to, filters)` +- `GetSCAPIMetrics(ctx, from, to, filters)` +- `GetSCAPIHooksMetrics(ctx, from, to, filters)` +- `GetMRTMetrics(ctx, from, to, filters)` +- `GetControllerMetrics(ctx, from, to, filters)` +- `GetOCAPIMetrics(ctx, from, to, filters)` + +**Features**: +- Auto-adds `sfcc.metrics` + tenant scope +- Normalizes timestamps (API seconds → milliseconds) +- Enriches series with structured tags +- 30-day retention enforcement + +**Example**: +```go +client := metrics.NewClient(metrics.Config{ + ShortCode: "kv7kzm78", + TenantID: "bdpx_prd", +}, authClient) + +filters := map[string]string{"apiFamily": "product"} +resp, err := client.GetSCAPIMetrics(ctx, from, to, filters) +``` + +### `clients/cip` + +JDBC-over-Avatica client for CIP analytics warehouse. + +**Key Types**: +- `Client`: CIP connection client +- `Config`: Configuration (host, tenantID) + +**Methods**: +- `Query(ctx, sql)`: Execute SQL query, returns `*sql.Rows` +- `QueryContext(ctx, sql, args...)`: Parameterized query +- `GetMetadata()`: Fetch table/column metadata +- `Close()`: Close connection + +**Features**: +- Sticky session handling (required by CIP) +- Calcite SQL dialect support +- Connection pooling (session-scoped) +- DECIMAL handling (transmitted as strings) + +**Example**: +```go +client, err := cip.NewClient(cip.Config{ + Host: "https://cip-us.commercecloud.salesforce.com", + TenantID: "bdpx_prd", +}, authClient) + +rows, err := client.Query(ctx, "SELECT * FROM orders LIMIT 10") +defer rows.Close() +``` + +### `clients` (tenant helpers) + +Tenant ID normalization and scope construction. + +**Functions**: +- `NormalizeTenantID(tenantID string) string`: Strip `f_ecom_` prefix +- `ToOrganizationID(tenantID string) string`: Add `f_ecom_` prefix if missing +- `BuildTenantScope(tenantID string) string`: Construct OAuth scope + +**Example**: +```go +normalized := clients.NormalizeTenantID("f_ecom_bdpx_prd") // "bdpx_prd" +orgID := clients.ToOrganizationID("bdpx_prd") // "f_ecom_bdpx_prd" +scope := clients.BuildTenantScope("bdpx_prd") // "SALESFORCE_COMMERCE_API:bdpx_prd" +``` + +### `operations/metrics` + +High-level operations for Metrics API. + +**Functions**: +- `ResolveMetricsWindow(from, to TimeInput) (time.Time, time.Time, error)`: Enforce 30-day retention +- `ParseSeriesTags(params ParseSeriesTagsParams) map[string]string`: Extract tags from series ID + +**Features**: +- Flexible time input (time.Time, epoch ms, ISO string, relative duration) +- 24-hour default window +- 30-day retention enforcement with 5-minute safety margin +- Tag extraction via declarative catalog (32 golden test cases) + +**Example**: +```go +// Resolve time window +from, to, err := metrics.ResolveMetricsWindow("2026-07-13T00:00:00Z", time.Now()) + +// Parse series tags +tags := metrics.ParseSeriesTags(metrics.ParseSeriesTagsParams{ + Category: "scapi", + MetricID: "totalCalls", + SeriesID: "bdpx.product", + Context: metrics.MetricsTagContext{TenantID: "f_ecom_bdpx_prd"}, +}) +// Result: {"realm": "bdpx", "environment": "prd", "apiFamily": "product"} +``` + +## Error Handling + +All client methods return typed errors: + +```go +resp, err := client.GetOverallMetrics(ctx, from, to, nil) +if err != nil { + // Check for specific error types + if errors.Is(err, auth.ErrInvalidCredentials) { + // Handle invalid OAuth credentials + } + if errors.Is(err, metrics.ErrRateLimited) { + // Handle 429 rate limiting + } + // Generic error handling + fmt.Printf("Error: %v\n", err) +} +``` + +**Common Errors**: +- `auth.ErrInvalidCredentials`: OAuth 401 (wrong client ID/secret) +- `auth.ErrForbidden`: OAuth 403 (insufficient scope) +- `metrics.ErrNotFound`: API 404 (wrong shortCode/tenantID) +- `metrics.ErrRateLimited`: API 429 (too many requests) +- `cip.ErrConnectionFailed`: CIP connection refused +- `cip.ErrQueryTimeout`: CIP query timeout + +## Testing + +The SDK includes comprehensive tests with 100% coverage: + +```bash +# Run all tests +go test ./... + +# Run with coverage +go test -cover ./... + +# Run specific package +go test -v ./auth +go test -v ./clients/metrics +go test -v ./clients/cip +go test -v ./operations/metrics +``` + +**Golden Test Fixtures**: +- `operations/metrics/data/metrics-tags.golden.json`: 32 tag extraction test cases +- `operations/metrics/data/metrics-tags-catalog.json`: Declarative tag rules +- Both files are identical to TypeScript SDK (parity enforced by `catalog_parity_test.go`) + +## Versioning + +This package is versioned via **git tags** (not npm changesets): +- Tag pattern: `go-sdk/vX.Y.Z` +- Example: `go-sdk/v0.1.0` +- Follows semantic versioning + +**Releases**: +- Major: Breaking API changes +- Minor: New features (backward compatible) +- Patch: Bug fixes + +**To depend on a specific version**: +```bash +go get github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go@go-sdk/v0.1.0 +``` + +## Architecture + +The SDK is organized into distinct layers: + +``` +b2c-tooling-sdk-go/ +├── auth/ # OAuth2 client credentials +│ ├── oauth.go # Token acquisition + caching +│ └── oauth_test.go +│ +├── clients/ # API clients +│ ├── tenant.go # Tenant ID helpers +│ ├── tenant_test.go +│ ├── metrics/ # Metrics API client +│ │ ├── client.go # 9 category endpoints +│ │ ├── client_test.go +│ │ ├── partition.go # Request partitioning (>24h) +│ │ └── partition_test.go +│ └── cip/ # CIP analytics client +│ ├── client.go # JDBC-over-Avatica +│ └── client_test.go +│ +├── operations/ # High-level operations +│ └── metrics/ +│ ├── window.go # Time window resolution +│ ├── window_test.go +│ ├── tags.go # Tag extraction +│ ├── tags_golden_test.go +│ ├── catalog_parity_test.go +│ └── data/ +│ ├── metrics-tags-catalog.json # Tag rules +│ └── metrics-tags.golden.json # Golden fixtures +│ +├── go.mod # Module definition +├── go.sum # Checksums +└── README.md # This file +``` + +**Design Principles**: +- No Grafana dependencies (standalone SDK) +- Testable (interfaces, mock servers) +- Typed errors (via `errors.Is`/`errors.As`) +- Idiomatic Go (context-aware, error handling) +- Parity with TypeScript SDK (same golden tests) + +## API Documentation + +Full API documentation is available via godoc: + +```bash +# Local godoc server +godoc -http=:6060 + +# Open http://localhost:6060/pkg/github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go/ +``` + +Or online at https://pkg.go.dev/github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go + +## Related Packages + +- **[b2c-grafana-datasource](../b2c-grafana-datasource/)**: Grafana plugins powered by this SDK +- **[b2c-tooling-sdk](../b2c-tooling-sdk/)**: TypeScript SDK (CLI + MCP) +- **[b2c-cli](../b2c-cli/)**: Command-line interface +- **[b2c-dx-mcp](../b2c-dx-mcp/)**: Model Context Protocol server + +## Contributing + +This SDK is part of the [b2c-developer-tooling](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling) monorepo. + +**Development workflow**: +1. Make changes +2. Add tests (`go test ./...`) +3. Update godoc comments +4. Ensure golden tests pass +5. Open PR + +**Golden test parity**: +- TypeScript catalog: `../b2c-tooling-sdk/specs/metrics-tags-catalog.json` +- Go catalog: `operations/metrics/data/metrics-tags-catalog.json` +- Must be byte-identical (enforced by `catalog_parity_test.go`) + +## License + +Copyright (c) 2025, Salesforce, Inc. Licensed under Apache-2.0. + +See [license.txt](../../license.txt) in repository root. + +## Support + +- **Issues**: https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/issues +- **Documentation**: https://developer.salesforce.com/docs/commerce/commerce-api +- **B2C CLI**: Related tooling at https://github.com/SalesforceCommerceCloud/b2c-developer-tooling diff --git a/packages/b2c-tooling-sdk-go/auth/oauth.go b/packages/b2c-tooling-sdk-go/auth/oauth.go new file mode 100644 index 000000000..bd17ae1f3 --- /dev/null +++ b/packages/b2c-tooling-sdk-go/auth/oauth.go @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +// Package auth provides OAuth2 authentication strategies for B2C Commerce APIs. +package auth + +import ( + "context" + "net/http" + "strings" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" +) + +const ( + // DefaultAccountManagerHost is the default Account Manager host for OAuth authentication. + DefaultAccountManagerHost = "account.demandware.com" + + // ClientIDHeader is the HTTP header used to pass the OAuth client ID. + ClientIDHeader = "x-dw-client-id" +) + +// OAuthConfig holds configuration for OAuth2 client credentials flow. +type OAuthConfig struct { + // ClientID is the OAuth client ID. + ClientID string + + // ClientSecret is the OAuth client secret. + ClientSecret string + + // AccountManagerHost is the Account Manager hostname. + // Defaults to DefaultAccountManagerHost if not specified. + // Ignored if TokenURL is set. + AccountManagerHost string + + // TokenURL is the full OAuth token endpoint URL. + // If set, used verbatim (supports http:// for local testing). + // If empty, derived from AccountManagerHost as https://{host}/dwsso/oauth2/access_token. + TokenURL string + + // Scopes is the list of OAuth scopes to request. + // If nil or empty, requests no scopes (server default). + Scopes []string +} + +// OAuthStrategy implements OAuth2 client credentials authentication with token caching +// and automatic x-dw-client-id header injection. It wraps golang.org/x/oauth2/clientcredentials +// for grant+refresh and provides an http.Client ready for B2C Commerce API calls. +type OAuthStrategy struct { + clientID string + config *clientcredentials.Config + client *http.Client +} + +// NewOAuthStrategy creates a new OAuth2 authentication strategy using client credentials flow. +// The returned strategy provides an http.Client that automatically: +// - Fetches and caches OAuth tokens +// - Refreshes tokens on expiry +// - Injects the Authorization: Bearer header +// - Injects the x-dw-client-id header +// +// Token caching is handled by oauth2.ReuseTokenSource, which provides single-flight token +// requests (concurrent calls coalesce onto a single token fetch). +func NewOAuthStrategy(cfg OAuthConfig) *OAuthStrategy { + // Determine token URL + var tokenURL string + if cfg.TokenURL != "" { + // Use explicit override (supports http:// for local testing) + tokenURL = cfg.TokenURL + } else { + // Derive from AccountManagerHost + host := cfg.AccountManagerHost + if host == "" { + host = DefaultAccountManagerHost + } + tokenURL = "https://" + host + "/dwsso/oauth2/access_token" + } + + // Create client credentials config + ccConfig := &clientcredentials.Config{ + ClientID: cfg.ClientID, + ClientSecret: cfg.ClientSecret, + TokenURL: tokenURL, + Scopes: cfg.Scopes, + AuthStyle: oauth2.AuthStyleInHeader, // Use Basic auth (base64 clientId:clientSecret) + } + + // Create an HTTP client with the oauth2 transport plus our custom header transport + ctx := context.Background() + tokenSource := ccConfig.TokenSource(ctx) + + // Wrap the oauth2 transport to inject x-dw-client-id header + baseClient := oauth2.NewClient(ctx, tokenSource) + clientIDTransport := &clientIDTransport{ + base: baseClient.Transport, + clientID: cfg.ClientID, + } + + client := &http.Client{ + Transport: clientIDTransport, + } + + return &OAuthStrategy{ + clientID: cfg.ClientID, + config: ccConfig, + client: client, + } +} + +// Client returns the http.Client configured with OAuth authentication. +// The client automatically handles token fetch, cache, refresh, and header injection. +func (s *OAuthStrategy) Client() *http.Client { + return s.client +} + +// WithAdditionalScopes returns a new OAuthStrategy with additional scopes added to the existing set. +// This is useful for creating scoped clients from a base strategy without re-specifying all config. +func (s *OAuthStrategy) WithAdditionalScopes(additionalScopes []string) *OAuthStrategy { + // Merge scopes, avoiding duplicates + scopeSet := make(map[string]bool) + for _, scope := range s.config.Scopes { + scopeSet[scope] = true + } + for _, scope := range additionalScopes { + scopeSet[scope] = true + } + + // Convert back to slice + mergedScopes := make([]string, 0, len(scopeSet)) + for scope := range scopeSet { + mergedScopes = append(mergedScopes, scope) + } + + // Create new config with merged scopes + newConfig := &clientcredentials.Config{ + ClientID: s.config.ClientID, + ClientSecret: s.config.ClientSecret, + TokenURL: s.config.TokenURL, + Scopes: mergedScopes, + AuthStyle: s.config.AuthStyle, + } + + // Create new client with merged scopes + ctx := context.Background() + tokenSource := newConfig.TokenSource(ctx) + baseClient := oauth2.NewClient(ctx, tokenSource) + clientIDTransport := &clientIDTransport{ + base: baseClient.Transport, + clientID: s.clientID, + } + + client := &http.Client{ + Transport: clientIDTransport, + } + + return &OAuthStrategy{ + clientID: s.clientID, + config: newConfig, + client: client, + } +} + +// clientIDTransport is an http.RoundTripper that injects the x-dw-client-id header. +type clientIDTransport struct { + base http.RoundTripper + clientID string +} + +func (t *clientIDTransport) RoundTrip(req *http.Request) (*http.Response, error) { + // Clone the request to avoid mutating the original + req2 := req.Clone(req.Context()) + req2.Header.Set(ClientIDHeader, t.clientID) + return t.base.RoundTrip(req2) +} + +// FormatScopes joins scopes into a space-delimited string for OAuth token requests. +func FormatScopes(scopes []string) string { + return strings.Join(scopes, " ") +} diff --git a/packages/b2c-tooling-sdk-go/auth/oauth_test.go b/packages/b2c-tooling-sdk-go/auth/oauth_test.go new file mode 100644 index 000000000..92ed39736 --- /dev/null +++ b/packages/b2c-tooling-sdk-go/auth/oauth_test.go @@ -0,0 +1,310 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +package auth + +import ( + "context" + "encoding/base64" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "golang.org/x/oauth2" +) + +func TestOAuthStrategy_Integration(t *testing.T) { + // Track token requests + var tokenRequests int + var lastAuthHeader string + + // Mock token endpoint + tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tokenRequests++ + + // Verify Basic auth header + lastAuthHeader = r.Header.Get("Authorization") + if !strings.HasPrefix(lastAuthHeader, "Basic ") { + t.Errorf("Expected Basic auth, got: %s", lastAuthHeader) + } + + // Decode Basic auth to verify client credentials + encodedCreds := strings.TrimPrefix(lastAuthHeader, "Basic ") + decoded, err := base64.StdEncoding.DecodeString(encodedCreds) + if err != nil { + t.Fatalf("Failed to decode Basic auth: %v", err) + } + creds := string(decoded) + if !strings.HasPrefix(creds, "test-client-id:test-client-secret") { + t.Errorf("Expected credentials 'test-client-id:test-client-secret', got: %s", creds) + } + + // Verify grant_type + body, _ := io.ReadAll(r.Body) + if !strings.Contains(string(body), "grant_type=client_credentials") { + t.Errorf("Expected grant_type=client_credentials in body, got: %s", string(body)) + } + + // Return mock token + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "access_token": "mock-access-token", + "token_type": "Bearer", + "expires_in": 3600, + "scope": "sfcc.metrics SALESFORCE_COMMERCE_API:bdpx_prd" + }`)) + })) + defer tokenServer.Close() + + // Mock API endpoint + apiCallCount := 0 + apiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + apiCallCount++ + + // Verify Bearer token + authHeader := r.Header.Get("Authorization") + if authHeader != "Bearer mock-access-token" { + t.Errorf("Expected Bearer token, got: %s", authHeader) + } + + // Verify x-dw-client-id header + clientIDHeader := r.Header.Get("x-dw-client-id") + if clientIDHeader != "test-client-id" { + t.Errorf("Expected x-dw-client-id header 'test-client-id', got: %s", clientIDHeader) + } + + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"status": "ok"}`)) + })) + defer apiServer.Close() + + // Create OAuth strategy with mock token URL + strategy := NewOAuthStrategy(OAuthConfig{ + ClientID: "test-client-id", + ClientSecret: "test-client-secret", + AccountManagerHost: strings.TrimPrefix(tokenServer.URL, "https://"), + Scopes: []string{"sfcc.metrics", "SALESFORCE_COMMERCE_API:bdpx_prd"}, + }) + + // Override the token URL since httptest creates http:// URLs + strategy.config.TokenURL = tokenServer.URL + + // Recreate the client with the updated config + ctx := context.Background() + tokenSource := strategy.config.TokenSource(ctx) + baseClient := &http.Client{ + Transport: &http.Transport{}, + } + baseClient.Transport = &oauth2Transport{ + base: baseClient.Transport, + source: tokenSource, + } + clientIDTransport := &clientIDTransport{ + base: baseClient.Transport, + clientID: strategy.clientID, + } + strategy.client = &http.Client{ + Transport: clientIDTransport, + } + + // Make first API call + resp, err := strategy.Client().Get(apiServer.URL) + if err != nil { + t.Fatalf("First API call failed: %v", err) + } + resp.Body.Close() + + if tokenRequests != 1 { + t.Errorf("Expected 1 token request, got %d", tokenRequests) + } + if apiCallCount != 1 { + t.Errorf("Expected 1 API call, got %d", apiCallCount) + } + + // Make second API call - should reuse cached token + resp, err = strategy.Client().Get(apiServer.URL) + if err != nil { + t.Fatalf("Second API call failed: %v", err) + } + resp.Body.Close() + + if tokenRequests != 1 { + t.Errorf("Expected token to be cached (still 1 request), got %d", tokenRequests) + } + if apiCallCount != 2 { + t.Errorf("Expected 2 API calls, got %d", apiCallCount) + } +} + +func TestOAuthStrategy_WithAdditionalScopes(t *testing.T) { + strategy := NewOAuthStrategy(OAuthConfig{ + ClientID: "test-client", + ClientSecret: "test-secret", + Scopes: []string{"sfcc.products"}, + }) + + // Add additional scopes + scopedStrategy := strategy.WithAdditionalScopes([]string{"sfcc.orders", "sfcc.customers"}) + + // Verify merged scopes (order may vary due to map iteration) + scopeMap := make(map[string]bool) + for _, scope := range scopedStrategy.config.Scopes { + scopeMap[scope] = true + } + + expectedScopes := []string{"sfcc.products", "sfcc.orders", "sfcc.customers"} + for _, expected := range expectedScopes { + if !scopeMap[expected] { + t.Errorf("Expected scope %q in merged scopes", expected) + } + } + + if len(scopedStrategy.config.Scopes) != 3 { + t.Errorf("Expected 3 merged scopes, got %d", len(scopedStrategy.config.Scopes)) + } +} + +func TestOAuthStrategy_WithAdditionalScopes_NoDuplicates(t *testing.T) { + strategy := NewOAuthStrategy(OAuthConfig{ + ClientID: "test-client", + ClientSecret: "test-secret", + Scopes: []string{"sfcc.products", "sfcc.orders"}, + }) + + // Add overlapping scopes + scopedStrategy := strategy.WithAdditionalScopes([]string{"sfcc.orders", "sfcc.customers"}) + + // Verify no duplicates + if len(scopedStrategy.config.Scopes) != 3 { + t.Errorf("Expected 3 unique scopes, got %d: %v", len(scopedStrategy.config.Scopes), scopedStrategy.config.Scopes) + } +} + +// oauth2Transport is a minimal oauth2 transport for testing (mimics oauth2.Transport) +type oauth2Transport struct { + base http.RoundTripper + source oauth2.TokenSource +} + +func (t *oauth2Transport) RoundTrip(req *http.Request) (*http.Response, error) { + token, err := t.source.Token() + if err != nil { + return nil, err + } + + req2 := req.Clone(req.Context()) + token.SetAuthHeader(req2) + return t.base.RoundTrip(req2) +} + +func TestOAuthStrategy_TokenURLOverride(t *testing.T) { + // Mock token endpoint + tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "access_token": "override-token", + "token_type": "Bearer", + "expires_in": 3600 + }`)) + })) + defer tokenServer.Close() + + // Create strategy with explicit TokenURL override (http://) + strategy := NewOAuthStrategy(OAuthConfig{ + ClientID: "test-client", + ClientSecret: "test-secret", + TokenURL: tokenServer.URL, // Full http:// URL + Scopes: []string{"sfcc.metrics"}, + }) + + // Verify the tokenURL was used verbatim + if strategy.config.TokenURL != tokenServer.URL { + t.Errorf("Expected TokenURL %q, got %q", tokenServer.URL, strategy.config.TokenURL) + } + + // Verify it accepts http:// scheme + if !strings.HasPrefix(strategy.config.TokenURL, "http://") { + t.Errorf("Expected http:// scheme to be preserved, got: %s", strategy.config.TokenURL) + } + + // Test that it actually works for token fetch + ctx := context.Background() + token, err := strategy.config.TokenSource(ctx).Token() + if err != nil { + t.Fatalf("Token fetch failed: %v", err) + } + + if token.AccessToken != "override-token" { + t.Errorf("Expected token 'override-token', got %q", token.AccessToken) + } +} + +func TestOAuthStrategy_TokenURLEmpty_FallsBackToAccountManagerHost(t *testing.T) { + // Create strategy without TokenURL (should derive from AccountManagerHost) + strategy := NewOAuthStrategy(OAuthConfig{ + ClientID: "test-client", + ClientSecret: "test-secret", + AccountManagerHost: "custom.account.manager.com", + }) + + expectedURL := "https://custom.account.manager.com/dwsso/oauth2/access_token" + if strategy.config.TokenURL != expectedURL { + t.Errorf("Expected TokenURL %q, got %q", expectedURL, strategy.config.TokenURL) + } +} + +func TestOAuthStrategy_TokenURLEmpty_DefaultAccountManager(t *testing.T) { + // Create strategy without TokenURL or AccountManagerHost + strategy := NewOAuthStrategy(OAuthConfig{ + ClientID: "test-client", + ClientSecret: "test-secret", + }) + + expectedURL := "https://" + DefaultAccountManagerHost + "/dwsso/oauth2/access_token" + if strategy.config.TokenURL != expectedURL { + t.Errorf("Expected TokenURL %q, got %q", expectedURL, strategy.config.TokenURL) + } +} + +func TestFormatScopes(t *testing.T) { + tests := []struct { + name string + scopes []string + want string + }{ + { + name: "single scope", + scopes: []string{"sfcc.products"}, + want: "sfcc.products", + }, + { + name: "multiple scopes", + scopes: []string{"sfcc.products", "sfcc.orders", "SALESFORCE_COMMERCE_API:bdpx_prd"}, + want: "sfcc.products sfcc.orders SALESFORCE_COMMERCE_API:bdpx_prd", + }, + { + name: "empty scopes", + scopes: []string{}, + want: "", + }, + { + name: "nil scopes", + scopes: nil, + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := FormatScopes(tt.scopes) + if got != tt.want { + t.Errorf("FormatScopes(%v) = %q, want %q", tt.scopes, got, tt.want) + } + }) + } +} diff --git a/packages/b2c-tooling-sdk-go/clients/cip/client.go b/packages/b2c-tooling-sdk-go/clients/cip/client.go new file mode 100644 index 000000000..491a83466 --- /dev/null +++ b/packages/b2c-tooling-sdk-go/clients/cip/client.go @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +// Package cip is a client for the B2C Commerce Intelligence Platform (CIP) analytics +// warehouse. CIP exposes an Apache Calcite Avatica endpoint over protobuf; this package +// wraps the maintained apache/calcite-avatica-go driver, injecting B2C OAuth +// authentication and the CIP-specific request headers and sticky-session handling. +package cip + +import ( + "context" + "database/sql" + "fmt" + "net/http" + "sync" + "time" + + avatica "github.com/apache/calcite-avatica-go/v5" + + "github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go/auth" +) + +const ( + // DefaultHost is the production CIP Avatica host. + DefaultHost = "jdbc.analytics.commercecloud.salesforce.com" + // clientVersion is sent as X-Client-Version, matching the reference client. + clientVersion = "2.11.0" +) + +// Config configures a CIP client. +type Config struct { + // Instance is the CIP instance identifier (e.g. "bdpx_prd"). Required. + Instance string + // Host overrides the CIP Avatica host. Empty → DefaultHost. + Host string +} + +// Client executes SQL against CIP via the Avatica protobuf protocol. +type Client struct { + db *sql.DB +} + +// QueryResult is a decoded CIP result set. +type QueryResult struct { + Columns []string + Rows []map[string]any +} + +// sessionTransport injects the CIP-required request headers (InstanceId, +// X-Client-Version) and implements Avatica sticky sessions: CIP returns an +// x-session-id on the first (OpenConnection) response that MUST be echoed on every +// subsequent request, or the load balancer routes to a backend that has no such +// connection and returns 410 Gone. avatica-go does not do this itself. +type sessionTransport struct { + base http.RoundTripper + instance string + + mu sync.Mutex + sessionID string +} + +func (t *sessionTransport) RoundTrip(req *http.Request) (*http.Response, error) { + req.Header.Set("InstanceId", t.instance) + req.Header.Set("X-Client-Version", clientVersion) + + t.mu.Lock() + sid := t.sessionID + t.mu.Unlock() + if sid != "" { + req.Header.Set("x-session-id", sid) + } + + resp, err := t.base.RoundTrip(req) + if err != nil { + return resp, err + } + if got := resp.Header.Get("x-session-id"); got != "" { + t.mu.Lock() + t.sessionID = got + t.mu.Unlock() + } + return resp, nil +} + +// NewClient builds a CIP client. The auth strategy is scoped for the instance +// (SALESFORCE_COMMERCE_API:{instance}); its OAuth http.Client carries the bearer token, +// wrapped with CIP header + sticky-session handling and handed to the Avatica driver. +// +// A CIP connection is sticky to one backend, so each Client owns a single-connection +// pool (MaxOpenConns=1); create multiple Clients for concurrency. +func NewClient(cfg Config, authStrategy *auth.OAuthStrategy) (*Client, error) { + if cfg.Instance == "" { + return nil, fmt.Errorf("cip: Instance is required") + } + host := cfg.Host + if host == "" { + host = DefaultHost + } + + scoped := authStrategy.WithAdditionalScopes([]string{"SALESFORCE_COMMERCE_API:" + cfg.Instance}) + oauthClient := scoped.Client() + + httpClient := &http.Client{ + Timeout: 60 * time.Second, + Transport: &sessionTransport{ + base: oauthClient.Transport, + instance: cfg.Instance, + }, + } + + dsn := fmt.Sprintf("https://%s/%s", host, cfg.Instance) + connector, ok := avatica.NewConnector(dsn).(*avatica.Connector) + if !ok { + return nil, fmt.Errorf("cip: unexpected connector type from avatica driver") + } + connector.Client = httpClient + + db := sql.OpenDB(connector) + // CIP sessions are sticky to a single backend; keep exactly one connection. + db.SetMaxOpenConns(1) + db.SetMaxIdleConns(1) + db.SetConnMaxLifetime(5 * time.Minute) + + return &Client{db: db}, nil +} + +// Query runs a SQL statement and returns all rows with column metadata. Values are +// decoded to their native Go types by the driver (numbers, strings, time.Time, etc.). +func (c *Client) Query(ctx context.Context, query string) (*QueryResult, error) { + rows, err := c.db.QueryContext(ctx, query) + if err != nil { + return nil, fmt.Errorf("cip query failed: %w", err) + } + defer rows.Close() + + cols, err := rows.Columns() + if err != nil { + return nil, fmt.Errorf("cip: reading columns: %w", err) + } + + result := &QueryResult{Columns: cols, Rows: []map[string]any{}} + for rows.Next() { + vals := make([]any, len(cols)) + ptrs := make([]any, len(cols)) + for i := range vals { + ptrs[i] = &vals[i] + } + if err := rows.Scan(ptrs...); err != nil { + return nil, fmt.Errorf("cip: scanning row: %w", err) + } + row := make(map[string]any, len(cols)) + for i, col := range cols { + row[col] = vals[i] + } + result.Rows = append(result.Rows, row) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("cip: iterating rows: %w", err) + } + return result, nil +} + +// DB exposes the underlying *sql.DB for callers that need direct database/sql access +// (e.g. Grafana's sqlutil frame conversion working on *sql.Rows). The pool is +// single-connection and sticky to one CIP backend; use QueryContext on it. +func (c *Client) DB() *sql.DB { + return c.db +} + +// Close releases the underlying connection pool. +func (c *Client) Close() error { + return c.db.Close() +} + +// Ping verifies connectivity by running a trivial metadata query. +func (c *Client) Ping(ctx context.Context) error { + _, err := c.Query(ctx, "SELECT tableName FROM metadata.TABLES LIMIT 1") + return err +} diff --git a/packages/b2c-tooling-sdk-go/clients/cip/client_test.go b/packages/b2c-tooling-sdk-go/clients/cip/client_test.go new file mode 100644 index 000000000..a6af01f93 --- /dev/null +++ b/packages/b2c-tooling-sdk-go/clients/cip/client_test.go @@ -0,0 +1,344 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +package cip + +import ( + "net/http" + "net/http/httptest" + "sync" + "testing" + + "github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go/auth" +) + +func TestNewClient_RequiresInstance(t *testing.T) { + mockAuth := createMockAuthStrategy(t) + + _, err := NewClient(Config{ + Instance: "", // Empty instance should error + }, mockAuth) + + if err == nil { + t.Fatal("Expected error for empty Instance, got nil") + } + if err.Error() != "cip: Instance is required" { + t.Errorf("Expected 'cip: Instance is required', got: %v", err) + } +} + +func TestNewClient_DefaultHost(t *testing.T) { + mockAuth := createMockAuthStrategy(t) + + client, err := NewClient(Config{ + Instance: "test_instance", + // Host empty → should default to DefaultHost + }, mockAuth) + + if err != nil { + t.Fatalf("Unexpected error creating client: %v", err) + } + if client == nil { + t.Fatal("Expected non-nil client") + } + // Clean up + client.Close() +} + +func TestNewClient_CustomHost(t *testing.T) { + mockAuth := createMockAuthStrategy(t) + + client, err := NewClient(Config{ + Instance: "test_instance", + Host: "custom.jdbc.host.com", + }, mockAuth) + + if err != nil { + t.Fatalf("Unexpected error creating client with custom host: %v", err) + } + if client == nil { + t.Fatal("Expected non-nil client") + } + // Clean up + client.Close() +} + +func TestSessionTransport_InjectsHeaders(t *testing.T) { + var capturedHeaders http.Header + var requestCount int + + // Mock HTTP server to capture headers + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestCount++ + capturedHeaders = r.Header.Clone() + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + // Create session transport + transport := &sessionTransport{ + base: http.DefaultTransport, + instance: "test_instance", + } + + // Make request + req, _ := http.NewRequest("GET", server.URL, nil) + client := &http.Client{Transport: transport} + resp, err := client.Do(req) + if err != nil { + t.Fatalf("Request failed: %v", err) + } + resp.Body.Close() + + // Verify headers were injected + if got := capturedHeaders.Get("InstanceId"); got != "test_instance" { + t.Errorf("Expected InstanceId header 'test_instance', got: %q", got) + } + if got := capturedHeaders.Get("X-Client-Version"); got != clientVersion { + t.Errorf("Expected X-Client-Version header %q, got: %q", clientVersion, got) + } + if requestCount != 1 { + t.Errorf("Expected 1 request, got %d", requestCount) + } +} + +func TestSessionTransport_StickySession(t *testing.T) { + var requests []http.Header + testSessionID := "test-session-123" + + // Mock HTTP server that returns x-session-id on first request + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requests = append(requests, r.Header.Clone()) + + // First request: return session ID + if len(requests) == 1 { + w.Header().Set("x-session-id", testSessionID) + } + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + // Create session transport + transport := &sessionTransport{ + base: http.DefaultTransport, + instance: "test_instance", + } + + client := &http.Client{Transport: transport} + + // First request - should NOT have x-session-id + req1, _ := http.NewRequest("GET", server.URL, nil) + resp1, err := client.Do(req1) + if err != nil { + t.Fatalf("First request failed: %v", err) + } + resp1.Body.Close() + + // Second request - SHOULD have x-session-id echoed + req2, _ := http.NewRequest("GET", server.URL, nil) + resp2, err := client.Do(req2) + if err != nil { + t.Fatalf("Second request failed: %v", err) + } + resp2.Body.Close() + + // Third request - SHOULD also have x-session-id + req3, _ := http.NewRequest("GET", server.URL, nil) + resp3, err := client.Do(req3) + if err != nil { + t.Fatalf("Third request failed: %v", err) + } + resp3.Body.Close() + + // Verify first request did NOT have session ID + if got := requests[0].Get("x-session-id"); got != "" { + t.Errorf("First request should not have x-session-id, got: %q", got) + } + + // Verify second request echoed session ID + if got := requests[1].Get("x-session-id"); got != testSessionID { + t.Errorf("Second request expected x-session-id %q, got: %q", testSessionID, got) + } + + // Verify third request also echoed session ID + if got := requests[2].Get("x-session-id"); got != testSessionID { + t.Errorf("Third request expected x-session-id %q, got: %q", testSessionID, got) + } +} + +func TestSessionTransport_ThreadSafety(t *testing.T) { + testSessionID := "concurrent-session-456" + requestCount := 0 + var mu sync.Mutex + + // Mock HTTP server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mu.Lock() + requestCount++ + count := requestCount + mu.Unlock() + + // Return session ID on first request + if count == 1 { + w.Header().Set("x-session-id", testSessionID) + } + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + // Create session transport + transport := &sessionTransport{ + base: http.DefaultTransport, + instance: "test_instance", + } + + client := &http.Client{Transport: transport} + + // Make concurrent requests to test thread safety + const concurrency = 10 + var wg sync.WaitGroup + errors := make(chan error, concurrency) + + for i := 0; i < concurrency; i++ { + wg.Add(1) + go func() { + defer wg.Done() + req, _ := http.NewRequest("GET", server.URL, nil) + resp, err := client.Do(req) + if err != nil { + errors <- err + return + } + resp.Body.Close() + }() + } + + wg.Wait() + close(errors) + + // Check for errors + for err := range errors { + t.Errorf("Concurrent request failed: %v", err) + } + + // Verify all requests completed + mu.Lock() + finalCount := requestCount + mu.Unlock() + + if finalCount != concurrency { + t.Errorf("Expected %d requests, got %d", concurrency, finalCount) + } +} + +func TestSessionTransport_SessionIDUpdate(t *testing.T) { + sessionIDs := []string{"session-1", "session-2", "session-3"} + requestNum := 0 + + // Mock HTTP server that changes session ID on each request + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if requestNum < len(sessionIDs) { + w.Header().Set("x-session-id", sessionIDs[requestNum]) + } + requestNum++ + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + transport := &sessionTransport{ + base: http.DefaultTransport, + instance: "test_instance", + } + + client := &http.Client{Transport: transport} + + // Make requests and verify session ID updates + for i := 0; i < len(sessionIDs); i++ { + req, _ := http.NewRequest("GET", server.URL, nil) + resp, err := client.Do(req) + if err != nil { + t.Fatalf("Request %d failed: %v", i, err) + } + resp.Body.Close() + + // Verify internal session ID was updated + transport.mu.Lock() + got := transport.sessionID + transport.mu.Unlock() + + if got != sessionIDs[i] { + t.Errorf("After request %d, expected sessionID %q, got %q", i, sessionIDs[i], got) + } + } +} + +// createMockAuthStrategy creates a minimal mock OAuth strategy for testing. +// It returns a strategy with a mock token endpoint that doesn't perform actual OAuth. +func createMockAuthStrategy(t *testing.T) *auth.OAuthStrategy { + // Create a mock token server + tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "access_token": "mock-token", + "token_type": "Bearer", + "expires_in": 3600 + }`)) + })) + t.Cleanup(tokenServer.Close) + + // Create strategy with mock token URL + strategy := auth.NewOAuthStrategy(auth.OAuthConfig{ + ClientID: "mock-client", + ClientSecret: "mock-secret", + TokenURL: tokenServer.URL, // Use mock server + Scopes: []string{"SALESFORCE_COMMERCE_API:test"}, + }) + + return strategy +} + +func TestClient_DB(t *testing.T) { + mockAuth := createMockAuthStrategy(t) + + client, err := NewClient(Config{ + Instance: "test_instance", + }, mockAuth) + if err != nil { + t.Fatalf("Failed to create client: %v", err) + } + defer client.Close() + + // Verify DB() returns non-nil + db := client.DB() + if db == nil { + t.Error("Expected non-nil *sql.DB from DB()") + } +} + +func TestClient_Close(t *testing.T) { + mockAuth := createMockAuthStrategy(t) + + client, err := NewClient(Config{ + Instance: "test_instance", + }, mockAuth) + if err != nil { + t.Fatalf("Failed to create client: %v", err) + } + + // Close should not error + if err := client.Close(); err != nil { + t.Errorf("Close() returned error: %v", err) + } + + // Multiple closes should be safe + if err := client.Close(); err != nil { + t.Errorf("Second Close() returned error: %v", err) + } +} + +// TestClient_Query and TestClient_Ping would require actual Avatica protocol mocking, +// which is complex. These are better tested via integration tests with a real CIP instance. +// The unit tests above cover the custom logic (session handling, config validation, header injection). diff --git a/packages/b2c-tooling-sdk-go/clients/cip/metadata.go b/packages/b2c-tooling-sdk-go/clients/cip/metadata.go new file mode 100644 index 000000000..76a34a393 --- /dev/null +++ b/packages/b2c-tooling-sdk-go/clients/cip/metadata.go @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +package cip + +import ( + "context" + "fmt" + "strings" +) + +// TableInfo describes a CIP catalog table. +type TableInfo struct { + Schema string `json:"schema"` + Name string `json:"name"` + Type string `json:"type"` +} + +// ColumnInfo describes a CIP table column. +type ColumnInfo struct { + Name string `json:"name"` + DataType string `json:"dataType"` + Nullable bool `json:"nullable"` + Ordinal int `json:"ordinal"` +} + +// asString coerces a decoded Avatica value to string. +func asString(v any) string { + switch t := v.(type) { + case string: + return t + case []byte: + return string(t) + case nil: + return "" + default: + return fmt.Sprintf("%v", t) + } +} + +// ListTables returns catalog tables, optionally filtered to a schema (e.g. "warehouse"). +func (c *Client) ListTables(ctx context.Context, schema string) ([]TableInfo, error) { + q := "SELECT tableSchem, tableName, tableType FROM metadata.TABLES" + if schema != "" { + q += fmt.Sprintf(" WHERE tableSchem = '%s'", escapeSQLString(schema)) + } + q += " ORDER BY tableSchem, tableName" + + res, err := c.Query(ctx, q) + if err != nil { + return nil, err + } + tables := make([]TableInfo, 0, len(res.Rows)) + for _, row := range res.Rows { + tables = append(tables, TableInfo{ + Schema: pick(row, "tableSchem", "tableschem", "TABLE_SCHEM"), + Name: pick(row, "tableName", "tablename", "TABLE_NAME"), + Type: pick(row, "tableType", "tabletype", "TABLE_TYPE"), + }) + } + return tables, nil +} + +// DescribeColumns returns the columns of a table (schema defaults to "warehouse"). +func (c *Client) DescribeColumns(ctx context.Context, schema, table string) ([]ColumnInfo, error) { + if schema == "" { + schema = "warehouse" + } + q := fmt.Sprintf( + "SELECT columnName, typeName, isNullable, ordinalPosition FROM metadata.COLUMNS "+ + "WHERE tableSchem = '%s' AND tableName = '%s' ORDER BY ordinalPosition", + escapeSQLString(schema), escapeSQLString(table), + ) + res, err := c.Query(ctx, q) + if err != nil { + return nil, err + } + cols := make([]ColumnInfo, 0, len(res.Rows)) + for _, row := range res.Rows { + cols = append(cols, ColumnInfo{ + Name: pick(row, "columnName", "columnname", "COLUMN_NAME"), + DataType: pick(row, "typeName", "typename", "TYPE_NAME"), + Nullable: strings.EqualFold(pick(row, "isNullable", "isnullable", "IS_NULLABLE"), "YES"), + Ordinal: pickInt(row, "ordinalPosition", "ordinalposition", "ORDINAL_POSITION"), + }) + } + return cols, nil +} + +// pick returns the first present key's value as a string (CIP/JDBC metadata label +// casing varies by driver: camelCase alias vs uppercase JDBC name). +func pick(row map[string]any, keys ...string) string { + for _, k := range keys { + if v, ok := row[k]; ok { + return asString(v) + } + } + return "" +} + +// pickInt returns the first present key's value coerced to int. +func pickInt(row map[string]any, keys ...string) int { + for _, k := range keys { + if v, ok := row[k]; ok { + switch t := v.(type) { + case int: + return t + case int32: + return int(t) + case int64: + return int(t) + case float64: + return int(t) + } + } + } + return 0 +} + +// escapeSQLString escapes single quotes for safe embedding in a SQL string literal. +func escapeSQLString(s string) string { + return strings.ReplaceAll(s, "'", "''") +} diff --git a/packages/b2c-tooling-sdk-go/clients/cip/metadata_test.go b/packages/b2c-tooling-sdk-go/clients/cip/metadata_test.go new file mode 100644 index 000000000..5e4b06896 --- /dev/null +++ b/packages/b2c-tooling-sdk-go/clients/cip/metadata_test.go @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +package cip + +import ( + "testing" +) + +func TestPick(t *testing.T) { + tests := []struct { + name string + row map[string]any + keys []string + expected string + }{ + { + name: "first key exists", + row: map[string]any{"columnName": "test_col", "COLUMN_NAME": "other"}, + keys: []string{"columnName", "COLUMN_NAME"}, + expected: "test_col", + }, + { + name: "second key exists", + row: map[string]any{"COLUMN_NAME": "uppercase_col"}, + keys: []string{"columnName", "COLUMN_NAME"}, + expected: "uppercase_col", + }, + { + name: "no keys exist", + row: map[string]any{"other": "value"}, + keys: []string{"columnName", "COLUMN_NAME"}, + expected: "", + }, + { + name: "nil value", + row: map[string]any{"columnName": nil}, + keys: []string{"columnName"}, + expected: "", + }, + { + name: "byte slice value", + row: map[string]any{"columnName": []byte("byte_col")}, + keys: []string{"columnName"}, + expected: "byte_col", + }, + { + name: "case sensitivity", + row: map[string]any{"columnname": "lowercase", "columnName": "camelCase"}, + keys: []string{"columnName", "columnname"}, + expected: "camelCase", + }, + { + name: "empty row", + row: map[string]any{}, + keys: []string{"columnName"}, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := pick(tt.row, tt.keys...) + if got != tt.expected { + t.Errorf("pick() = %q, want %q", got, tt.expected) + } + }) + } +} + +func TestPickInt(t *testing.T) { + tests := []struct { + name string + row map[string]any + keys []string + expected int + }{ + { + name: "int value", + row: map[string]any{"ordinalPosition": 5}, + keys: []string{"ordinalPosition"}, + expected: 5, + }, + { + name: "int32 value", + row: map[string]any{"ordinalPosition": int32(10)}, + keys: []string{"ordinalPosition"}, + expected: 10, + }, + { + name: "int64 value", + row: map[string]any{"ordinalPosition": int64(15)}, + keys: []string{"ordinalPosition"}, + expected: 15, + }, + { + name: "float64 value", + row: map[string]any{"ordinalPosition": float64(20.7)}, + keys: []string{"ordinalPosition"}, + expected: 20, + }, + { + name: "first key exists", + row: map[string]any{"ordinalposition": 1, "ORDINAL_POSITION": 2}, + keys: []string{"ordinalposition", "ORDINAL_POSITION"}, + expected: 1, + }, + { + name: "no keys exist", + row: map[string]any{"other": 42}, + keys: []string{"ordinalPosition"}, + expected: 0, + }, + { + name: "string value (not int)", + row: map[string]any{"ordinalPosition": "not_a_number"}, + keys: []string{"ordinalPosition"}, + expected: 0, + }, + { + name: "nil value", + row: map[string]any{"ordinalPosition": nil}, + keys: []string{"ordinalPosition"}, + expected: 0, + }, + { + name: "empty row", + row: map[string]any{}, + keys: []string{"ordinalPosition"}, + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := pickInt(tt.row, tt.keys...) + if got != tt.expected { + t.Errorf("pickInt() = %d, want %d", got, tt.expected) + } + }) + } +} + +func TestEscapeSQLString(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "no single quotes", + input: "warehouse", + expected: "warehouse", + }, + { + name: "single quote", + input: "O'Reilly", + expected: "O''Reilly", + }, + { + name: "multiple single quotes", + input: "It's John's book", + expected: "It''s John''s book", + }, + { + name: "only single quote", + input: "'", + expected: "''", + }, + { + name: "consecutive single quotes", + input: "''test''", + expected: "''''test''''", + }, + { + name: "empty string", + input: "", + expected: "", + }, + { + name: "SQL injection attempt", + input: "'; DROP TABLE users; --", + expected: "''; DROP TABLE users; --", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := escapeSQLString(tt.input) + if got != tt.expected { + t.Errorf("escapeSQLString(%q) = %q, want %q", tt.input, got, tt.expected) + } + }) + } +} + +func TestAsString(t *testing.T) { + tests := []struct { + name string + input any + expected string + }{ + { + name: "string value", + input: "test_string", + expected: "test_string", + }, + { + name: "byte slice", + input: []byte("byte_string"), + expected: "byte_string", + }, + { + name: "nil value", + input: nil, + expected: "", + }, + { + name: "int value", + input: 42, + expected: "42", + }, + { + name: "float value", + input: 3.14, + expected: "3.14", + }, + { + name: "bool value", + input: true, + expected: "true", + }, + { + name: "empty string", + input: "", + expected: "", + }, + { + name: "empty byte slice", + input: []byte{}, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := asString(tt.input) + if got != tt.expected { + t.Errorf("asString(%v) = %q, want %q", tt.input, got, tt.expected) + } + }) + } +} diff --git a/packages/b2c-tooling-sdk-go/clients/metrics/client.go b/packages/b2c-tooling-sdk-go/clients/metrics/client.go new file mode 100644 index 000000000..fbae7bd19 --- /dev/null +++ b/packages/b2c-tooling-sdk-go/clients/metrics/client.go @@ -0,0 +1,261 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +// Package metrics provides a typed client for the B2C Commerce Metrics API. +package metrics + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go/auth" + "github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go/clients" + metricsops "github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go/operations/metrics" +) + +const ( + // MetricsScope is the required OAuth scope for the Metrics API. + MetricsScope = "sfcc.metrics" +) + +// DataPoint represents a single data point in a time series. +// Timestamp is epoch MILLISECONDS (normalized from the API's epoch seconds). +type DataPoint struct { + Timestamp int64 `json:"timestamp"` // Epoch milliseconds + Value float64 `json:"value"` +} + +// DataSeries represents a single time series within a metric. +type DataSeries struct { + ID string `json:"id"` + Name string `json:"name"` + Data []DataPoint `json:"data"` + Tags metricsops.MetricSeriesTags `json:"tags,omitempty"` // Enriched tags +} + +// Metric represents a single metric with its time series. +type Metric struct { + MetricID string `json:"metricId"` + Title string `json:"title"` + Description string `json:"description"` + Unit string `json:"unit,omitempty"` + DataSeries []DataSeries `json:"dataSeries"` +} + +// MetricsDataResponse is the response from a metrics API call. +type MetricsDataResponse struct { + Data []Metric `json:"data"` +} + +// Config holds configuration for creating a Metrics client. +type Config struct { + // ShortCode is the SCAPI instance short code (e.g., "kv7kzm78"). + // Ignored if BaseURL is set. + ShortCode string + + // TenantID is the tenant ID (with or without f_ecom_ prefix). + // Used to build the organizationId path parameter and tenant-specific OAuth scope. + TenantID string + + // BaseURL is the full Metrics API base URL including the /observability/metrics/v1 path. + // If set, used verbatim (supports http:// for local testing). + // If empty, derived from ShortCode as https://{shortCode}.api.commercecloud.salesforce.com/observability/metrics/v1. + BaseURL string +} + +// Client is a typed client for the Metrics API. +type Client struct { + baseURL string + httpClient *http.Client + tenantID string +} + +// NewClient creates a new Metrics API client. +// The auth strategy should be an OAuthStrategy with appropriate scopes. +// The client automatically handles: +// - OAuth token management +// - Tenant-specific scopes (SALESFORCE_COMMERCE_API:{tenant}) +// - Timestamp normalization (epoch seconds → milliseconds) +// - Tag enrichment on responses +func NewClient(cfg Config, authStrategy *auth.OAuthStrategy) *Client { + // Build required scopes + requiredScopes := []string{MetricsScope, clients.BuildTenantScope(cfg.TenantID)} + + // Create scoped auth client + scopedAuth := authStrategy.WithAdditionalScopes(requiredScopes) + + // Determine base URL + var baseURL string + if cfg.BaseURL != "" { + // Use explicit override (supports http:// for local testing) + baseURL = cfg.BaseURL + } else { + // Derive from ShortCode + baseURL = fmt.Sprintf("https://%s.api.commercecloud.salesforce.com/observability/metrics/v1", cfg.ShortCode) + } + + return &Client{ + baseURL: baseURL, + httpClient: scopedAuth.Client(), + tenantID: cfg.TenantID, + } +} + +// getMetrics is the common method for all category endpoints. +func (c *Client) getMetrics(ctx context.Context, category string, from, to time.Time, filters map[string]string) (*MetricsDataResponse, error) { + orgID := clients.ToOrganizationID(c.tenantID) + path := fmt.Sprintf("/organizations/%s/metrics/%s", orgID, category) + + // Build query params + query := url.Values{} + query.Set("from", strconv.FormatInt(from.Unix(), 10)) + query.Set("to", strconv.FormatInt(to.Unix(), 10)) + + // Add category-specific filters + for k, v := range filters { + if v != "" { + query.Set(k, v) + } + } + + fullURL := c.baseURL + path + "?" + query.Encode() + + req, err := http.NewRequestWithContext(ctx, "GET", fullURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to execute request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + // Bound the error-body read so a large/hostile error page can't exhaust memory. + body, _ := io.ReadAll(io.LimitReader(resp.Body, 8*1024)) + return nil, &HTTPError{ + StatusCode: resp.StatusCode, + Status: resp.Status, + Body: string(body), + RetryAfter: parseRetryAfter(resp.Header.Get("Retry-After")), + } + } + + var data MetricsDataResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + // Normalize timestamps (epoch seconds → milliseconds) + normalizeTimestamps(&data) + + // Enrich with tags + enrichWithTags(&data, category, c.tenantID, filters) + + return &data, nil +} + +// secondsThreshold is an epoch value below which a timestamp is assumed to be in +// seconds (roughly year 2286 in seconds / year 1970+ in ms). Used to make timestamp +// normalization idempotent so a response that is normalized twice (e.g. served from a +// future response cache) is not scaled to a nonsense far-future instant. +const secondsThreshold = int64(1e11) + +// normalizeTimestamps converts data-point timestamps from the API's epoch seconds to +// epoch milliseconds (Go/Grafana convention). It is idempotent: a point already in +// milliseconds (>= secondsThreshold) is left unchanged, so re-normalizing a response is +// a no-op rather than multiplying by 1000 again. +func normalizeTimestamps(resp *MetricsDataResponse) { + for i := range resp.Data { + for j := range resp.Data[i].DataSeries { + for k := range resp.Data[i].DataSeries[j].Data { + if resp.Data[i].DataSeries[j].Data[k].Timestamp < secondsThreshold { + resp.Data[i].DataSeries[j].Data[k].Timestamp *= 1000 + } + } + } + } +} + +// enrichWithTags adds structured tags to each series based on the series ID and request context. +func enrichWithTags(resp *MetricsDataResponse, category, tenantID string, filters map[string]string) { + context := metricsops.MetricsTagContext{ + TenantID: tenantID, + APIFamily: filters["apiFamily"], + APIName: filters["apiName"], + OcapiCategory: filters["ocapiCategory"], + OcapiAPI: filters["ocapiApi"], + ThirdPartyServiceID: filters["thirdPartyServiceId"], + } + + for i := range resp.Data { + metricID := resp.Data[i].MetricID + for j := range resp.Data[i].DataSeries { + series := &resp.Data[i].DataSeries[j] + series.Tags = metricsops.ParseSeriesTags(metricsops.ParseSeriesTagsParams{ + Category: category, + MetricID: metricID, + SeriesID: series.ID, + Context: context, + }) + } + } +} + +// GetOverallMetrics retrieves overall application metrics. +func (c *Client) GetOverallMetrics(ctx context.Context, from, to time.Time, filters map[string]string) (*MetricsDataResponse, error) { + return c.getMetricsRange(ctx, "overall", from, to, filters) +} + +// GetSalesMetrics retrieves sales metrics. +func (c *Client) GetSalesMetrics(ctx context.Context, from, to time.Time, filters map[string]string) (*MetricsDataResponse, error) { + return c.getMetricsRange(ctx, "sales", from, to, filters) +} + +// GetEcdnMetrics retrieves eCDN metrics. +func (c *Client) GetEcdnMetrics(ctx context.Context, from, to time.Time, filters map[string]string) (*MetricsDataResponse, error) { + return c.getMetricsRange(ctx, "ecdn", from, to, filters) +} + +// GetThirdPartyMetrics retrieves third-party service metrics. +// Optionally filter by thirdPartyServiceId. +func (c *Client) GetThirdPartyMetrics(ctx context.Context, from, to time.Time, filters map[string]string) (*MetricsDataResponse, error) { + return c.getMetricsRange(ctx, "third-party", from, to, filters) +} + +// GetScapiMetrics retrieves SCAPI metrics. +// Optionally filter by apiFamily and/or apiName. +func (c *Client) GetScapiMetrics(ctx context.Context, from, to time.Time, filters map[string]string) (*MetricsDataResponse, error) { + return c.getMetricsRange(ctx, "scapi", from, to, filters) +} + +// GetScapiHooksMetrics retrieves SCAPI hooks metrics. +func (c *Client) GetScapiHooksMetrics(ctx context.Context, from, to time.Time, filters map[string]string) (*MetricsDataResponse, error) { + return c.getMetricsRange(ctx, "scapi-hooks", from, to, filters) +} + +// GetMrtMetrics retrieves MRT (Managed Runtime) metrics. +func (c *Client) GetMrtMetrics(ctx context.Context, from, to time.Time, filters map[string]string) (*MetricsDataResponse, error) { + return c.getMetricsRange(ctx, "mrt", from, to, filters) +} + +// GetControllerMetrics retrieves controller metrics. +func (c *Client) GetControllerMetrics(ctx context.Context, from, to time.Time, filters map[string]string) (*MetricsDataResponse, error) { + return c.getMetricsRange(ctx, "controller", from, to, filters) +} + +// GetOcapiMetrics retrieves OCAPI metrics. +// Optionally filter by ocapiCategory and/or ocapiApi. +func (c *Client) GetOcapiMetrics(ctx context.Context, from, to time.Time, filters map[string]string) (*MetricsDataResponse, error) { + return c.getMetricsRange(ctx, "ocapi", from, to, filters) +} diff --git a/packages/b2c-tooling-sdk-go/clients/metrics/client_test.go b/packages/b2c-tooling-sdk-go/clients/metrics/client_test.go new file mode 100644 index 000000000..ab87fbf8e --- /dev/null +++ b/packages/b2c-tooling-sdk-go/clients/metrics/client_test.go @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +package metrics + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go/auth" +) + +func TestNewClient_BaseURLOverride(t *testing.T) { + // Mock token server + tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"access_token": "test-token", "token_type": "Bearer", "expires_in": 3600}`)) + })) + defer tokenServer.Close() + + // Mock metrics API server + metricsServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Verify path structure + expectedPath := "/organizations/f_ecom_bdpx_prd/metrics/overall" + if r.URL.Path != expectedPath { + t.Errorf("Expected path %q, got %q", expectedPath, r.URL.Path) + } + + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "data": [{ + "metricId": "totalCalls", + "title": "Total Calls", + "description": "Total API calls", + "unit": "", + "dataSeries": [{ + "id": "bdpx.test", + "name": "test", + "data": [{"timestamp": 1000, "value": 100}] + }] + }] + }`)) + })) + defer metricsServer.Close() + + // Create auth strategy with token override + authStrategy := auth.NewOAuthStrategy(auth.OAuthConfig{ + ClientID: "test-client", + ClientSecret: "test-secret", + TokenURL: tokenServer.URL, + }) + + // Create client with BaseURL override (http://) + client := NewClient(Config{ + ShortCode: "ignored", // Should be ignored when BaseURL is set + TenantID: "bdpx_prd", + BaseURL: metricsServer.URL, // Full http:// URL + }, authStrategy) + + // Verify the baseURL was set correctly + if client.baseURL != metricsServer.URL { + t.Errorf("Expected baseURL %q, got %q", metricsServer.URL, client.baseURL) + } + + // Verify it accepts http:// scheme + if !strings.HasPrefix(client.baseURL, "http://") { + t.Errorf("Expected http:// scheme to be preserved, got: %s", client.baseURL) + } + + // Test actual API call to verify it works + ctx := context.Background() + from := time.Unix(500, 0) + to := time.Unix(2000, 0) + + resp, err := client.GetOverallMetrics(ctx, from, to, nil) + if err != nil { + t.Fatalf("GetOverallMetrics failed: %v", err) + } + + if len(resp.Data) != 1 { + t.Errorf("Expected 1 metric, got %d", len(resp.Data)) + } + + if resp.Data[0].MetricID != "totalCalls" { + t.Errorf("Expected metricId 'totalCalls', got %q", resp.Data[0].MetricID) + } +} + +func TestNewClient_BaseURLEmpty_DerivesFromShortCode(t *testing.T) { + // Mock token server + tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"access_token": "test-token", "token_type": "Bearer", "expires_in": 3600}`)) + })) + defer tokenServer.Close() + + authStrategy := auth.NewOAuthStrategy(auth.OAuthConfig{ + ClientID: "test-client", + ClientSecret: "test-secret", + TokenURL: tokenServer.URL, + }) + + // Create client without BaseURL (should derive from ShortCode) + client := NewClient(Config{ + ShortCode: "kv7kzm78", + TenantID: "bdpx_prd", + }, authStrategy) + + expectedURL := "https://kv7kzm78.api.commercecloud.salesforce.com/observability/metrics/v1" + if client.baseURL != expectedURL { + t.Errorf("Expected baseURL %q, got %q", expectedURL, client.baseURL) + } +} + +func TestNewClient_ScopesIncluded(t *testing.T) { + // Mock token server that captures the scope request + var requestedScopes string + tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Parse form to get scope parameter + r.ParseForm() + requestedScopes = r.Form.Get("scope") + + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"access_token": "test-token", "token_type": "Bearer", "expires_in": 3600}`)) + })) + defer tokenServer.Close() + + authStrategy := auth.NewOAuthStrategy(auth.OAuthConfig{ + ClientID: "test-client", + ClientSecret: "test-secret", + TokenURL: tokenServer.URL, + }) + + // Create client + client := NewClient(Config{ + ShortCode: "test", + TenantID: "bdpx_prd", + }, authStrategy) + + // Trigger a token fetch by making a request + ctx := context.Background() + from := time.Unix(500, 0) + to := time.Unix(2000, 0) + + // This will fail because we don't have a metrics server, but it will trigger token fetch + _, _ = client.GetOverallMetrics(ctx, from, to, nil) + + // Verify both required scopes are present + if !strings.Contains(requestedScopes, MetricsScope) { + t.Errorf("Expected scope %q in request, got: %s", MetricsScope, requestedScopes) + } + + // BuildTenantScope normalizes the tenant ID (strips f_ecom_), so expect the normalized form + expectedTenantScope := "SALESFORCE_COMMERCE_API:bdpx_prd" + if !strings.Contains(requestedScopes, expectedTenantScope) { + t.Errorf("Expected scope %q in request, got: %s", expectedTenantScope, requestedScopes) + } +} diff --git a/packages/b2c-tooling-sdk-go/clients/metrics/errors.go b/packages/b2c-tooling-sdk-go/clients/metrics/errors.go new file mode 100644 index 000000000..da4a93a55 --- /dev/null +++ b/packages/b2c-tooling-sdk-go/clients/metrics/errors.go @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +package metrics + +import ( + "errors" + "fmt" + "net/http" + "strconv" + "time" +) + +// HTTPError is a typed, inspectable error for a non-2xx Metrics API response. It lets +// callers (e.g. the Grafana backend) distinguish rate-limits (429) and downstream 5xx +// from client mistakes (4xx) and attribute the error correctly. Use errors.As to extract. +type HTTPError struct { + StatusCode int + Status string + Body string + // RetryAfter is the parsed Retry-After header (0 if absent/unparseable). + RetryAfter time.Duration +} + +func (e *HTTPError) Error() string { + if e.RetryAfter > 0 { + return fmt.Sprintf("metrics API error (status %d): %s (retry after %s)", e.StatusCode, e.Body, e.RetryAfter) + } + return fmt.Sprintf("metrics API error (status %d): %s", e.StatusCode, e.Body) +} + +// IsRateLimit reports whether this is a 429 Too Many Requests. +func (e *HTTPError) IsRateLimit() bool { return e.StatusCode == http.StatusTooManyRequests } + +// IsDownstream reports whether the failure is attributable to the upstream API rather +// than the plugin/caller: 429 and 5xx are downstream; 4xx (except 429) are caller errors. +func (e *HTTPError) IsDownstream() bool { + return e.StatusCode == http.StatusTooManyRequests || (e.StatusCode >= 500 && e.StatusCode < 600) +} + +// IsRetryable reports whether retrying the request could succeed (429, 5xx, 408). +func (e *HTTPError) IsRetryable() bool { + return e.StatusCode == http.StatusTooManyRequests || + e.StatusCode == http.StatusRequestTimeout || + (e.StatusCode >= 500 && e.StatusCode < 600) +} + +// AsHTTPError extracts an *HTTPError from an error chain, if present. +func AsHTTPError(err error) (*HTTPError, bool) { + var he *HTTPError + if errors.As(err, &he) { + return he, true + } + return nil, false +} + +// parseRetryAfter parses an HTTP Retry-After header value (delta-seconds or HTTP-date). +// Returns 0 when empty or unparseable. +func parseRetryAfter(header string) time.Duration { + if header == "" { + return 0 + } + if secs, err := strconv.Atoi(header); err == nil && secs >= 0 { + return time.Duration(secs) * time.Second + } + if t, err := http.ParseTime(header); err == nil { + if d := time.Until(t); d > 0 { + return d + } + } + return 0 +} diff --git a/packages/b2c-tooling-sdk-go/clients/metrics/partition.go b/packages/b2c-tooling-sdk-go/clients/metrics/partition.go new file mode 100644 index 000000000..c9c50573e --- /dev/null +++ b/packages/b2c-tooling-sdk-go/clients/metrics/partition.go @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +package metrics + +import ( + "context" + "sort" + "time" +) + +// maxWindow is the Metrics API's maximum time window per request. Requests wider than +// this are rejected by the API with a 400, so a wider range must be partitioned into +// sub-windows and merged. Kept in sync with operations/metrics.MetricsDefaultWindow (24h). +const maxWindow = 24 * time.Hour + +// getMetricsRange fetches a category over an arbitrary [from, to] range, transparently +// partitioning ranges wider than the API's 24-hour maximum into sequential sub-requests +// and merging the results by (metricId, seriesId). This lets a normal Grafana dashboard +// range like "Last 7 days" work against an API that only accepts <=24h windows. +// +// Sub-windows are fetched newest-first is unnecessary — order does not matter since we +// merge and sort by timestamp. Points at chunk boundaries are de-duplicated. +func (c *Client) getMetricsRange(ctx context.Context, category string, from, to time.Time, filters map[string]string) (*MetricsDataResponse, error) { + if !to.After(from) || to.Sub(from) <= maxWindow { + // Single request suffices. + return c.getMetrics(ctx, category, from, to, filters) + } + + // Accumulate merged series keyed by metricId then seriesId, preserving metric/series + // metadata from the first chunk that carries it. + type mergedMetric struct { + metric Metric // header (metricId/title/description/unit) + series map[string]*DataSeries // seriesID → series (with growing Data) + order []string // seriesID order of first appearance + } + metrics := map[string]*mergedMetric{} + var metricOrder []string + + // Walk sub-windows [start, end] with end-exclusive stitching so boundary points + // aren't double-counted: each chunk covers (start, start+maxWindow]. + for start := from; start.Before(to); start = start.Add(maxWindow) { + end := start.Add(maxWindow) + if end.After(to) { + end = to + } + chunk, err := c.getMetrics(ctx, category, start, end, filters) + if err != nil { + return nil, err + } + for _, m := range chunk.Data { + mm := metrics[m.MetricID] + if mm == nil { + mm = &mergedMetric{metric: m, series: map[string]*DataSeries{}} + metrics[m.MetricID] = mm + metricOrder = append(metricOrder, m.MetricID) + } + for _, s := range m.DataSeries { + existing := mm.series[s.ID] + if existing == nil { + cp := s // copy header (ID/Name/Tags) + cp.Data = append([]DataPoint(nil), s.Data...) + mm.series[s.ID] = &cp + mm.order = append(mm.order, s.ID) + continue + } + existing.Data = append(existing.Data, s.Data...) + } + } + } + + // Rebuild the response, sorting + de-duplicating each series' points by timestamp. + out := &MetricsDataResponse{Data: make([]Metric, 0, len(metricOrder))} + for _, mid := range metricOrder { + mm := metrics[mid] + metric := mm.metric + metric.DataSeries = make([]DataSeries, 0, len(mm.order)) + for _, sid := range mm.order { + s := mm.series[sid] + s.Data = sortDedupPoints(s.Data) + metric.DataSeries = append(metric.DataSeries, *s) + } + out.Data = append(out.Data, metric) + } + return out, nil +} + +// sortDedupPoints sorts points ascending by timestamp and drops duplicate timestamps +// (keeping the first), which can occur at sub-window boundaries. +func sortDedupPoints(points []DataPoint) []DataPoint { + if len(points) < 2 { + return points + } + sort.SliceStable(points, func(i, j int) bool { return points[i].Timestamp < points[j].Timestamp }) + out := points[:1] + for _, p := range points[1:] { + if p.Timestamp != out[len(out)-1].Timestamp { + out = append(out, p) + } + } + return out +} diff --git a/packages/b2c-tooling-sdk-go/clients/metrics/partition_test.go b/packages/b2c-tooling-sdk-go/clients/metrics/partition_test.go new file mode 100644 index 000000000..f75485ce1 --- /dev/null +++ b/packages/b2c-tooling-sdk-go/clients/metrics/partition_test.go @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +package metrics + +import "testing" + +func TestSortDedupPoints(t *testing.T) { + in := []DataPoint{ + {Timestamp: 300, Value: 3}, + {Timestamp: 100, Value: 1}, + {Timestamp: 200, Value: 2}, + {Timestamp: 200, Value: 2}, // duplicate boundary point + {Timestamp: 100, Value: 1}, // duplicate + } + got := sortDedupPoints(in) + want := []int64{100, 200, 300} + if len(got) != len(want) { + t.Fatalf("len=%d want %d (%v)", len(got), len(want), got) + } + for i, ts := range want { + if got[i].Timestamp != ts { + t.Errorf("point %d: ts=%d want %d", i, got[i].Timestamp, ts) + } + } +} + +func TestSortDedupPointsShort(t *testing.T) { + if got := sortDedupPoints(nil); got != nil { + t.Errorf("nil in → %v", got) + } + one := []DataPoint{{Timestamp: 5, Value: 1}} + if got := sortDedupPoints(one); len(got) != 1 { + t.Errorf("single point mangled: %v", got) + } +} diff --git a/packages/b2c-tooling-sdk-go/clients/tenant.go b/packages/b2c-tooling-sdk-go/clients/tenant.go new file mode 100644 index 000000000..f06d1ba41 --- /dev/null +++ b/packages/b2c-tooling-sdk-go/clients/tenant.go @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +// Package clients provides tenant ID normalization and OAuth scope helpers for B2C Commerce APIs. +package clients + +import "strings" + +const ( + // OrganizationIDPrefix is the required prefix for SCAPI organization IDs. + OrganizationIDPrefix = "f_ecom_" + + // ScapiTenantScopePrefix is the prefix for tenant-specific SCAPI OAuth scopes. + ScapiTenantScopePrefix = "SALESFORCE_COMMERCE_API:" +) + +// NormalizeTenantID normalizes a tenant ID by: +// 1. Trimming whitespace +// 2. Taking substring before first dot (if present) +// 3. Stripping leading "f_ecom_" prefix (if present) +// 4. Replacing all hyphens with underscores +// +// Examples: +// - "f_ecom_bdpx_prd" → "bdpx_prd" +// - "abcd-123.dx.commercecloud.salesforce.com" → "abcd_123" +// - "bdpx_prd" → "bdpx_prd" +func NormalizeTenantID(value string) string { + // Trim whitespace + normalized := strings.TrimSpace(value) + + // Take substring before first dot + if dotIdx := strings.Index(normalized, "."); dotIdx > 0 { + normalized = normalized[:dotIdx] + } + + // Strip f_ecom_ prefix + normalized = strings.TrimPrefix(normalized, OrganizationIDPrefix) + + // Replace hyphens with underscores + normalized = strings.ReplaceAll(normalized, "-", "_") + + return normalized +} + +// ToOrganizationID ensures a tenant ID has the required f_ecom_ prefix for use as an SCAPI organizationId. +// If the value already has the prefix, it is returned unchanged (after normalization). +// +// Examples: +// - "bdpx_prd" → "f_ecom_bdpx_prd" +// - "f_ecom_bdpx_prd" → "f_ecom_bdpx_prd" +func ToOrganizationID(tenantID string) string { + normalized := NormalizeTenantID(tenantID) + return OrganizationIDPrefix + normalized +} + +// BuildTenantScope constructs the tenant-specific SCAPI OAuth scope for the given tenant ID. +// +// Example: +// - "bdpx_prd" → "SALESFORCE_COMMERCE_API:bdpx_prd" +func BuildTenantScope(tenantID string) string { + normalized := NormalizeTenantID(tenantID) + return ScapiTenantScopePrefix + normalized +} diff --git a/packages/b2c-tooling-sdk-go/clients/tenant_test.go b/packages/b2c-tooling-sdk-go/clients/tenant_test.go new file mode 100644 index 000000000..011f18440 --- /dev/null +++ b/packages/b2c-tooling-sdk-go/clients/tenant_test.go @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +package clients + +import "testing" + +func TestNormalizeTenantID(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + { + name: "prefixed org id", + input: "f_ecom_bdpx_prd", + want: "bdpx_prd", + }, + { + name: "bare tenant id", + input: "bdpx_prd", + want: "bdpx_prd", + }, + { + name: "full hostname with dot", + input: "abcd-123.dx.commercecloud.salesforce.com", + want: "abcd_123", + }, + { + name: "hyphenated id", + input: "test-realm-env", + want: "test_realm_env", + }, + { + name: "with leading and trailing whitespace", + input: " bdpx_prd ", + want: "bdpx_prd", + }, + { + name: "prefixed with dot", + input: "f_ecom_bdpx_prd.dx.example.com", + want: "bdpx_prd", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := NormalizeTenantID(tt.input) + if got != tt.want { + t.Errorf("NormalizeTenantID(%q) = %q, want %q", tt.input, got, tt.want) + } + }) + } +} + +func TestToOrganizationID(t *testing.T) { + tests := []struct { + name string + tenantID string + want string + }{ + { + name: "bare tenant id", + tenantID: "bdpx_prd", + want: "f_ecom_bdpx_prd", + }, + { + name: "already prefixed", + tenantID: "f_ecom_bdpx_prd", + want: "f_ecom_bdpx_prd", + }, + { + name: "with hostname", + tenantID: "abcd-123.dx.example.com", + want: "f_ecom_abcd_123", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ToOrganizationID(tt.tenantID) + if got != tt.want { + t.Errorf("ToOrganizationID(%q) = %q, want %q", tt.tenantID, got, tt.want) + } + }) + } +} + +func TestBuildTenantScope(t *testing.T) { + tests := []struct { + name string + tenantID string + want string + }{ + { + name: "bare tenant id", + tenantID: "bdpx_prd", + want: "SALESFORCE_COMMERCE_API:bdpx_prd", + }, + { + name: "prefixed org id", + tenantID: "f_ecom_bdpx_prd", + want: "SALESFORCE_COMMERCE_API:bdpx_prd", + }, + { + name: "hyphenated id", + tenantID: "test-realm-env", + want: "SALESFORCE_COMMERCE_API:test_realm_env", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := BuildTenantScope(tt.tenantID) + if got != tt.want { + t.Errorf("BuildTenantScope(%q) = %q, want %q", tt.tenantID, got, tt.want) + } + }) + } +} diff --git a/packages/b2c-tooling-sdk-go/go.mod b/packages/b2c-tooling-sdk-go/go.mod new file mode 100644 index 000000000..17394f6e4 --- /dev/null +++ b/packages/b2c-tooling-sdk-go/go.mod @@ -0,0 +1,23 @@ +module github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go + +go 1.26 + +require ( + github.com/apache/calcite-avatica-go/v5 v5.4.0 + golang.org/x/oauth2 v0.24.0 +) + +require ( + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/icholy/digest v1.1.0 // indirect + github.com/jcmturner/aescts/v2 v2.0.0 // indirect + github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect + github.com/jcmturner/gofork v1.7.6 // indirect + github.com/jcmturner/goidentity/v6 v6.0.1 // indirect + github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect + github.com/jcmturner/rpc/v2 v2.0.3 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/net v0.33.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect +) diff --git a/packages/b2c-tooling-sdk-go/go.sum b/packages/b2c-tooling-sdk-go/go.sum new file mode 100644 index 000000000..40537ca2f --- /dev/null +++ b/packages/b2c-tooling-sdk-go/go.sum @@ -0,0 +1,85 @@ +github.com/apache/calcite-avatica-go/v5 v5.4.0 h1:snCrhGlwDgqNA2Rp7RUABjNX2zX+EfLk5K7PSJRPD5w= +github.com/apache/calcite-avatica-go/v5 v5.4.0/go.mod h1:ed2DNx4xLzxrVYbvZU9Nv97LwyO6c0J7oGnOP4HbqZk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4= +github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/packages/b2c-tooling-sdk-go/operations/metrics/catalog_parity_test.go b/packages/b2c-tooling-sdk-go/operations/metrics/catalog_parity_test.go new file mode 100644 index 000000000..244485c90 --- /dev/null +++ b/packages/b2c-tooling-sdk-go/operations/metrics/catalog_parity_test.go @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2.0 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +package metrics + +import ( + "encoding/json" + "os" + "path/filepath" + "reflect" + "runtime" + "testing" +) + +// TestCatalogParity ensures the Go-embedded metrics tag catalog and golden +// fixture remain in sync with the TypeScript source of truth. +// +// The canonical catalog lives in packages/b2c-tooling-sdk/specs/: +// - metrics-tags-catalog.json (tag extraction rules) +// - metrics-tags.golden.json (test fixture with expected outputs) +// +// The Go SDK embeds copies at operations/metrics/data/. This test fails if the +// Go copies drift from the TS originals (ignoring the volatile generatedAt +// timestamp field). +// +// Why cross-language parity matters: +// - The TS SDK generates the catalog via npm script; the Go SDK embeds it. +// - Both implementations must agree on tag extraction logic for Grafana/CLI consistency. +// - A mismatch would cause silent query/tagging divergence between tools. +// +// If this test fails: +// 1. Regenerate the TS catalog: cd packages/b2c-tooling-sdk && pnpm run generate:metrics-tags-catalog +// 2. Copy the updated JSON files from packages/b2c-tooling-sdk/specs/ to +// packages/b2c-tooling-sdk-go/operations/metrics/data/ +// 3. Re-run this test to verify parity. +func TestCatalogParity(t *testing.T) { + // Find the repo root by walking up from this test file's directory. + // This is robust to different working directories (go test ./..., CI, IDE test runners). + _, thisFile, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("Failed to determine test file path") + } + + // Navigate from this test file to repo root: + // catalog_parity_test.go is at: + // packages/b2c-tooling-sdk-go/operations/metrics/catalog_parity_test.go + // repo root is 4 levels up: ../../../.. + repoRoot := filepath.Join(filepath.Dir(thisFile), "..", "..", "..", "..") + repoRoot, err := filepath.Abs(repoRoot) + if err != nil { + t.Fatalf("Failed to resolve repo root: %v", err) + } + + // TS source of truth paths + tsCatalogPath := filepath.Join(repoRoot, "packages", "b2c-tooling-sdk", "specs", "metrics-tags-catalog.json") + tsGoldenPath := filepath.Join(repoRoot, "packages", "b2c-tooling-sdk", "specs", "metrics-tags.golden.json") + + // Go embedded copies (checked against TS originals) + goCatalogPath := filepath.Join(repoRoot, "packages", "b2c-tooling-sdk-go", "operations", "metrics", "data", "metrics-tags-catalog.json") + goGoldenPath := filepath.Join(repoRoot, "packages", "b2c-tooling-sdk-go", "operations", "metrics", "data", "metrics-tags.golden.json") + + // If TS files don't exist, skip with a clear message (this allows the test + // to pass outside the monorepo or when the SDK package is absent). + if _, err := os.Stat(tsCatalogPath); os.IsNotExist(err) { + t.Skip("TypeScript catalog not found; skipping cross-language parity check (this is OK outside the monorepo)") + } + + // Compare catalog files + t.Run("catalog", func(t *testing.T) { + assertJSONFilesEqual(t, tsCatalogPath, goCatalogPath) + }) + + // Compare golden fixture files + t.Run("golden", func(t *testing.T) { + assertJSONFilesEqual(t, tsGoldenPath, goGoldenPath) + }) +} + +// assertJSONFilesEqual reads two JSON files, parses them, removes volatile fields +// (like generatedAt timestamps), and asserts deep equality of the meaningful content. +func assertJSONFilesEqual(t *testing.T, expectedPath, actualPath string) { + t.Helper() + + // Read expected (TS source of truth) + expectedBytes, err := os.ReadFile(expectedPath) + if err != nil { + t.Fatalf("Failed to read expected file %s: %v", expectedPath, err) + } + + // Read actual (Go embedded copy) + actualBytes, err := os.ReadFile(actualPath) + if err != nil { + t.Fatalf("Failed to read actual file %s: %v", actualPath, err) + } + + // Parse both as generic JSON + var expected, actual map[string]interface{} + if err := json.Unmarshal(expectedBytes, &expected); err != nil { + t.Fatalf("Failed to parse expected JSON from %s: %v", expectedPath, err) + } + if err := json.Unmarshal(actualBytes, &actual); err != nil { + t.Fatalf("Failed to parse actual JSON from %s: %v", actualPath, err) + } + + // Remove volatile generatedAt timestamp before comparison + delete(expected, "generatedAt") + delete(actual, "generatedAt") + + // Deep-compare the meaningful content + if !reflect.DeepEqual(expected, actual) { + // Format both for readable diff output + expectedJSON, _ := json.MarshalIndent(expected, "", " ") + actualJSON, _ := json.MarshalIndent(actual, "", " ") + + t.Errorf("Go catalog drift detected!\n\nExpected (TS source of truth):\n%s\n\nActual (Go embedded copy):\n%s\n\nTo fix:\n 1. cd packages/b2c-tooling-sdk && pnpm run generate:metrics-tags-catalog\n 2. cp packages/b2c-tooling-sdk/specs/*.json packages/b2c-tooling-sdk-go/operations/metrics/data/\n 3. Re-run tests", + expectedJSON, actualJSON) + } +} diff --git a/packages/b2c-tooling-sdk-go/operations/metrics/data/metrics-tags-catalog.json b/packages/b2c-tooling-sdk-go/operations/metrics/data/metrics-tags-catalog.json new file mode 100644 index 000000000..24ff9b14a --- /dev/null +++ b/packages/b2c-tooling-sdk-go/operations/metrics/data/metrics-tags-catalog.json @@ -0,0 +1,87 @@ +{ + "version": "1.0.0", + "generatedAt": "2026-07-14T18:47:24.331Z", + "description": "Declarative catalog of metrics series tag extraction rules. Defines how packed series ids are parsed into structured dimension tags.", + "strategies": [ + "familyOrStatus", + "familyOrOverallAgg", + "lastSpaceSplit", + "lastDotSplit", + "wholeAs", + "ecdnSuccessError" + ], + "rules": [ + { + "category": "scapi", + "metricId": "totalCalls", + "strategy": "familyOrStatus" + }, + { + "category": "scapi", + "metricId": "requestLatency", + "strategy": "familyOrOverallAgg" + }, + { + "category": "scapi", + "metricId": "responseCount", + "strategy": "familyOrStatus" + }, + { + "category": "scapi", + "metricId": "errors4xx", + "strategy": "wholeAs", + "key": "apiFamily" + }, + { + "category": "scapi", + "metricId": "cacheHitRate", + "strategy": "lastSpaceSplit" + }, + { + "category": "ocapi", + "metricId": "totalCalls", + "strategy": "wholeAs", + "key": "ocapiCategory" + }, + { + "category": "ocapi", + "metricId": "callsMean", + "strategy": "wholeAs", + "key": "ocapiCategory" + }, + { + "category": "controller", + "metricId": "*", + "strategy": "wholeAs", + "key": "controller" + }, + { + "category": "third-party", + "metricId": "callsCount", + "strategy": "wholeAs", + "key": "host" + }, + { + "category": "third-party", + "metricId": "callsP95", + "strategy": "wholeAs", + "key": "host" + }, + { + "category": "third-party", + "metricId": "remoteExceptions", + "strategy": "lastDotSplit" + }, + { + "category": "ecdn", + "metricId": "successAndError", + "strategy": "ecdnSuccessError" + }, + { + "category": "ecdn", + "metricId": "*", + "strategy": "wholeAs", + "key": "host" + } + ] +} diff --git a/packages/b2c-tooling-sdk-go/operations/metrics/data/metrics-tags.golden.json b/packages/b2c-tooling-sdk-go/operations/metrics/data/metrics-tags.golden.json new file mode 100644 index 000000000..5ecd0308d --- /dev/null +++ b/packages/b2c-tooling-sdk-go/operations/metrics/data/metrics-tags.golden.json @@ -0,0 +1,552 @@ +{ + "version": "1.0.0", + "generatedAt": "2026-07-14T18:48:22.984Z", + "description": "Golden test fixture for metrics tags extraction. Covers all strategies and edge cases. Both TS and Go tests assert against this.", + "testCases": [ + { + "category": "scapi", + "metricId": "totalCalls", + "seriesId": "bdpx.product", + "context": { + "tenantId": "f_ecom_bdpx_prd" + }, + "description": "Derives realm and environment from prefixed org id", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "apiFamily": "product" + } + }, + { + "category": "scapi", + "metricId": "totalCalls", + "seriesId": "bdpx.product", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "Derives realm and environment from bare tenant id", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "apiFamily": "product" + } + }, + { + "category": "overall", + "metricId": "requests", + "seriesId": "acme Requests", + "context": { + "tenantId": "acme" + }, + "description": "Realm with no environment (no underscore)", + "expectedTags": { + "realm": "acme", + "series": "Requests" + } + }, + { + "category": "scapi", + "metricId": "totalCalls", + "seriesId": "multi_underscore_realm_env.product", + "context": { + "tenantId": "multi_underscore_realm_env" + }, + "description": "Multi-segment tenant id (environment is last segment)", + "expectedTags": { + "realm": "multi_underscore_realm", + "environment": "env", + "apiFamily": "multi_underscore_realm_env", + "apiName": "product" + } + }, + { + "category": "scapi", + "metricId": "totalCalls", + "seriesId": "bdpx.product", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "SCAPI totalCalls: apiFamily", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "apiFamily": "product" + } + }, + { + "category": "scapi", + "metricId": "responseCount", + "seriesId": "bdpx 2xx", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "SCAPI responseCount: statusClass (not apiFamily)", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "statusClass": "2xx" + } + }, + { + "category": "scapi", + "metricId": "responseCount", + "seriesId": "bdpx 4xx", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "SCAPI responseCount: statusClass 4xx", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "statusClass": "4xx" + } + }, + { + "category": "scapi", + "metricId": "requestLatency", + "seriesId": "bdpx Average overall latency", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "SCAPI requestLatency: overall aggregation", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "aggregation": "overall" + } + }, + { + "category": "scapi", + "metricId": "requestLatency", + "seriesId": "bdpx.product", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "SCAPI requestLatency: apiFamily (not overall)", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "apiFamily": "product" + } + }, + { + "category": "scapi", + "metricId": "totalCalls", + "seriesId": "bdpx.shopper.auth.v1", + "context": { + "tenantId": "bdpx_prd", + "apiFamily": "shopper" + }, + "description": "SCAPI drill-down: apiFamily + apiName + apiVersion (filter overrides family)", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "apiFamily": "shopper", + "apiVersion": "v1", + "apiName": "auth" + } + }, + { + "category": "scapi", + "metricId": "requestLatency", + "seriesId": "bdpx.search.shopper-search.v1", + "context": { + "tenantId": "bdpx_prd", + "apiFamily": "search" + }, + "description": "SCAPI drill-down: hyphenated apiName + version", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "apiFamily": "search", + "apiVersion": "v1", + "apiName": "shopper-search" + } + }, + { + "category": "scapi", + "metricId": "totalCalls", + "seriesId": "bdpx.shopper.baskets.v2", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "SCAPI drill-down without filter: family/name/version all from id", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "apiFamily": "shopper", + "apiVersion": "v2", + "apiName": "baskets" + } + }, + { + "category": "scapi", + "metricId": "cacheHitRate", + "seriesId": "cacheHitRate", + "context": { + "tenantId": "bdpx_prd", + "apiFamily": "shopper" + }, + "description": "SCAPI rollup: cacheHitRate id echoes metric id → aggregation=total (filter still applies)", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "aggregation": "total", + "apiFamily": "shopper" + } + }, + { + "category": "scapi", + "metricId": "errors4xx", + "seriesId": "errors4xx", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "SCAPI rollup: errors4xx id echoes metric id → aggregation=total (not apiFamily)", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "aggregation": "total" + } + }, + { + "category": "scapi", + "metricId": "cacheHitRate", + "seriesId": "bdpx.product HIT", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "SCAPI cacheHitRate: apiFamily + cacheStatus split on space", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "apiFamily": "product", + "cacheStatus": "HIT" + } + }, + { + "category": "scapi", + "metricId": "cacheHitRate", + "seriesId": "bdpx.custom MISS", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "SCAPI cacheHitRate: MISS status", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "apiFamily": "custom", + "cacheStatus": "MISS" + } + }, + { + "category": "scapi", + "metricId": "cacheHitRate", + "seriesId": "bdpx.product", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "SCAPI cacheHitRate: no space (apiFamily only)", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "apiFamily": "product" + } + }, + { + "category": "scapi", + "metricId": "errors4xx", + "seriesId": "bdpx.shopper", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "SCAPI errors4xx: whole remainder as apiFamily", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "apiFamily": "shopper" + } + }, + { + "category": "ecdn", + "metricId": "successAndError", + "seriesId": "2xx bdpx.bdpx-prod_cc-bm_net", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "eCDN successAndError: status class before realm", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "statusClass": "2xx", + "host": "bdpx-prod_cc-bm_net" + } + }, + { + "category": "ecdn", + "metricId": "successAndError", + "seriesId": "5xx bdpx.bdpx-stg_host", + "context": { + "tenantId": "bdpx_stg" + }, + "description": "eCDN successAndError: 5xx status", + "expectedTags": { + "realm": "bdpx", + "environment": "stg", + "statusClass": "5xx", + "host": "bdpx-stg_host" + } + }, + { + "category": "ecdn", + "metricId": "successAndError", + "seriesId": "bdpx.bdpx-prod_host", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "eCDN successAndError: no status (host only)", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "host": "bdpx-prod_host" + } + }, + { + "category": "ecdn", + "metricId": "totalRequests", + "seriesId": "bdpx.bdpx-prod_cc-bm_net", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "eCDN other metrics: host only", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "host": "bdpx-prod_cc-bm_net" + } + }, + { + "category": "third-party", + "metricId": "callsCount", + "seriesId": "bdpx.login.salesforce.com", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "third-party callsCount: dotted host as whole remainder", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "host": "login.salesforce.com" + } + }, + { + "category": "third-party", + "metricId": "callsP95", + "seriesId": "bdpx.api.example.com", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "third-party callsP95: dotted host", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "host": "api.example.com" + } + }, + { + "category": "third-party", + "metricId": "remoteExceptions", + "seriesId": "bdpx.xitgmcd3.api.commercecloud.salesforce.com.socketReadTimeout", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "third-party remoteExceptions: host + exceptionType (last dot split)", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "host": "xitgmcd3.api.commercecloud.salesforce.com", + "exceptionType": "socketReadTimeout" + } + }, + { + "category": "third-party", + "metricId": "remoteExceptions", + "seriesId": "bdpx.login.salesforce.com.connectionTimeout", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "third-party remoteExceptions: another exception type", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "host": "login.salesforce.com", + "exceptionType": "connectionTimeout" + } + }, + { + "category": "third-party", + "metricId": "remoteExceptions", + "seriesId": "bdpx.host", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "third-party remoteExceptions: no dot (host only)", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "host": "host" + } + }, + { + "category": "ocapi", + "metricId": "totalCalls", + "seriesId": "bdpx.shop", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "OCAPI totalCalls: ocapiCategory", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "ocapiCategory": "shop" + } + }, + { + "category": "ocapi", + "metricId": "callsMean", + "seriesId": "bdpx.data", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "OCAPI callsMean: ocapiCategory", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "ocapiCategory": "data" + } + }, + { + "category": "controller", + "metricId": "callsMean", + "seriesId": "bdpx.Checkout-Begin", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "Controller wildcard: controller name", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "controller": "Checkout-Begin" + } + }, + { + "category": "controller", + "metricId": "totalCalls", + "seriesId": "bdpx.Home-Show", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "Controller wildcard: applies to any controller metric", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "controller": "Home-Show" + } + }, + { + "category": "scapi", + "metricId": "totalCalls", + "seriesId": "bdpx.shopper.auth.v1", + "context": { + "tenantId": "bdpx_prd", + "apiFamily": "shopper" + }, + "description": "Applied apiFamily filter overrides drill-down id", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "apiFamily": "shopper", + "apiVersion": "v1", + "apiName": "auth" + } + }, + { + "category": "scapi", + "metricId": "totalCalls", + "seriesId": "totalCalls", + "context": { + "tenantId": "bdpx_prd", + "apiFamily": "products", + "apiName": "shopper-products" + }, + "description": "Applied apiFamily + apiName filters", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "aggregation": "total", + "apiFamily": "products", + "apiName": "shopper-products" + } + }, + { + "category": "ocapi", + "metricId": "totalCalls", + "seriesId": "totalCalls", + "context": { + "tenantId": "bdpx_prd", + "ocapiCategory": "shop" + }, + "description": "Applied ocapiCategory filter", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "aggregation": "total", + "ocapiCategory": "shop" + } + }, + { + "category": "third-party", + "metricId": "callsCount", + "seriesId": "bdpx.login.salesforce.com", + "context": { + "tenantId": "bdpx_prd", + "thirdPartyServiceId": "my.svc" + }, + "description": "Applied thirdPartyServiceId filter (heuristic host still present)", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "host": "login.salesforce.com", + "thirdPartyServiceId": "my.svc" + } + }, + { + "category": "overall", + "metricId": "requests", + "seriesId": "bdpx Requests", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "Unrecognized pattern: prose label under series tag", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "series": "Requests" + } + }, + { + "category": "mrt", + "metricId": "errorRate", + "seriesId": "errorRate", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "Fallback series echoes metric id (not captured as series tag)", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "aggregation": "total" + } + } + ] +} diff --git a/packages/b2c-tooling-sdk-go/operations/metrics/tags.go b/packages/b2c-tooling-sdk-go/operations/metrics/tags.go new file mode 100644 index 000000000..4c5c976d7 --- /dev/null +++ b/packages/b2c-tooling-sdk-go/operations/metrics/tags.go @@ -0,0 +1,320 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +// Package metrics provides high-level operations for the Metrics API, including +// time-window resolution and series tag extraction. +package metrics + +import ( + _ "embed" + "encoding/json" + "regexp" + "strings" + + "github.com/SalesforceCommerceCloud/b2c-developer-tooling/packages/b2c-tooling-sdk-go/clients" +) + +//go:embed data/metrics-tags-catalog.json +var catalogJSON []byte + +// MetricSeriesTags is a flat map of a series' identifying dimensions (string → string), +// following the InfluxDB/Prometheus/CloudWatch "tag"/"label"/"dimension" convention. +// +// Always contains `realm` and optionally `environment` (derived from the request context). +// Category-specific keys (apiFamily, host, cacheStatus, statusClass, ocapiCategory, +// controller, exceptionType) are added when recognized. +type MetricSeriesTags map[string]string + +// MetricsTagContext holds the request identity and applied filters used to derive authoritative tags. +// +// realm/environment are parsed from tenantID. The optional filter fields mirror the Metrics API's +// category filters; when a filter was sent, that dimension is known from the request and is stamped +// onto every series as an authoritative tag. +type MetricsTagContext struct { + // TenantID is the tenant or organization id the request targeted (with or without f_ecom_). + TenantID string + + // APIFamily is the apiFamily filter sent with a scapi request, if any. + APIFamily string + + // APIName is the apiName filter sent with a scapi request, if any. + APIName string + + // OcapiCategory is the ocapiCategory filter sent with an ocapi request, if any. + OcapiCategory string + + // OcapiAPI is the ocapiApi filter sent with an ocapi request, if any. + OcapiAPI string + + // ThirdPartyServiceID is the thirdPartyServiceId filter sent with a third-party request, if any. + ThirdPartyServiceID string +} + +// ExtractorStrategy defines the strategy name for parsing series IDs. +type ExtractorStrategy string + +const ( + StrategyFamilyOrStatus ExtractorStrategy = "familyOrStatus" + StrategyFamilyOrOverallAgg ExtractorStrategy = "familyOrOverallAgg" + StrategyLastSpaceSplit ExtractorStrategy = "lastSpaceSplit" + StrategyLastDotSplit ExtractorStrategy = "lastDotSplit" + StrategyWholeAs ExtractorStrategy = "wholeAs" + StrategyEcdnSuccessError ExtractorStrategy = "ecdnSuccessError" +) + +// ExtractorRule defines a category/metricId-specific extraction rule. +type ExtractorRule struct { + Category string `json:"category"` + MetricID string `json:"metricId"` + Strategy ExtractorStrategy `json:"strategy"` + Key string `json:"key,omitempty"` // For wholeAs strategy +} + +// ExtractorCatalog holds all extraction rules loaded from the embedded JSON. +type ExtractorCatalog struct { + Version string `json:"version"` + GeneratedAt string `json:"generatedAt"` + Description string `json:"description"` + Strategies []string `json:"strategies"` + Rules []ExtractorRule `json:"rules"` +} + +var catalog ExtractorCatalog +var extractorIndex map[string]map[string]ExtractorRule // category -> metricId -> rule + +func init() { + if err := json.Unmarshal(catalogJSON, &catalog); err != nil { + panic("Failed to load metrics-tags-catalog.json: " + err.Error()) + } + + // Build lookup index + extractorIndex = make(map[string]map[string]ExtractorRule) + for _, rule := range catalog.Rules { + if extractorIndex[rule.Category] == nil { + extractorIndex[rule.Category] = make(map[string]ExtractorRule) + } + extractorIndex[rule.Category][rule.MetricID] = rule + } +} + +// splitRealmEnvironment splits a normalized tenant ID (bdpx_prd) into its realm and environment. +// The environment is the final underscore-delimited segment; everything before it is the realm. +// IDs without an underscore yield just a realm. +func splitRealmEnvironment(tenantID string) (realm string, environment string) { + normalized := clients.NormalizeTenantID(tenantID) + lastUnderscore := strings.LastIndex(normalized, "_") + if lastUnderscore <= 0 || lastUnderscore == len(normalized)-1 { + return normalized, "" + } + return normalized[:lastUnderscore], normalized[lastUnderscore+1:] +} + +// stripRealmPrefix strips a leading "realm." or "realm " prefix from a packed series id, if present. +// Returns the input unchanged when no realm prefix matches. +// +// Parity note: this MUST match the TS stripRealmPrefix (tags.ts) exactly — it strips +// ONLY the realm prefix, never the full normalized tenant id. A multi-underscore tenant +// (realm derived from all-but-last segment) therefore does NOT strip an id like +// "_.product"; the remainder stays whole. (normalizedTenantID is retained in +// the signature for call-site uniformity but is intentionally unused.) +func stripRealmPrefix(seriesID, realm, normalizedTenantID string) string { + _ = normalizedTenantID + if strings.HasPrefix(seriesID, realm+".") { + return seriesID[len(realm)+1:] + } + if strings.HasPrefix(seriesID, realm+" ") { + return seriesID[len(realm)+1:] + } + return seriesID +} + +// Strategy implementations + +var statusClassRegex = regexp.MustCompile(`^[1-5]xx$`) +var overallRegex = regexp.MustCompile(`(?i)overall`) +var apiVersionRegex = regexp.MustCompile(`^v\d+$`) + +// scapiDrilldown parses a SCAPI drill-down remainder into its dimensions. When a +// request filters by apiFamily, the server returns finer-grained series ids of the +// form {apiFamily}.{apiName}[.{apiName...}].{version} — e.g. "shopper.auth.v1" or +// "search.shopper-search.v1". It splits off the leading family, a trailing vN +// version (if present), and treats everything in between as the api name, so that +// otherwise-identical drilled-down series get distinct, groupable tags. apiFamily +// is authoritative-overridden later by any applied filter. Mirrors the TS scapiDrilldown. +func scapiDrilldown(remainder string) MetricSeriesTags { + segments := strings.Split(remainder, ".") + tags := MetricSeriesTags{"apiFamily": segments[0]} + rest := segments[1:] + if len(rest) > 0 && apiVersionRegex.MatchString(rest[len(rest)-1]) { + tags["apiVersion"] = rest[len(rest)-1] + rest = rest[:len(rest)-1] + } + if len(rest) > 0 { + tags["apiName"] = strings.Join(rest, ".") + } + return tags +} + +func strategyFamilyOrStatus(remainder, rawID, realm, normalizedTenantID, key string) MetricSeriesTags { + if statusClassRegex.MatchString(remainder) { + return MetricSeriesTags{"statusClass": remainder} + } + // A drill-down id ("shopper.auth.v1") carries an api name/version; a bare + // family ("product") does not. + if strings.Contains(remainder, ".") { + return scapiDrilldown(remainder) + } + return MetricSeriesTags{"apiFamily": remainder} +} + +func strategyFamilyOrOverallAgg(remainder, rawID, realm, normalizedTenantID, key string) MetricSeriesTags { + if overallRegex.MatchString(remainder) { + return MetricSeriesTags{"aggregation": "overall"} + } + if strings.Contains(remainder, ".") { + return scapiDrilldown(remainder) + } + return MetricSeriesTags{"apiFamily": remainder} +} + +func strategyLastSpaceSplit(remainder, rawID, realm, normalizedTenantID, key string) MetricSeriesTags { + spaceIdx := strings.LastIndex(remainder, " ") + if spaceIdx > 0 { + return MetricSeriesTags{ + "apiFamily": remainder[:spaceIdx], + "cacheStatus": remainder[spaceIdx+1:], + } + } + return MetricSeriesTags{"apiFamily": remainder} +} + +func strategyLastDotSplit(remainder, rawID, realm, normalizedTenantID, key string) MetricSeriesTags { + lastDot := strings.LastIndex(remainder, ".") + if lastDot > 0 { + return MetricSeriesTags{ + "host": remainder[:lastDot], + "exceptionType": remainder[lastDot+1:], + } + } + return MetricSeriesTags{"host": remainder} +} + +func strategyWholeAs(remainder, rawID, realm, normalizedTenantID, key string) MetricSeriesTags { + if key == "" { + panic("wholeAs strategy requires a key parameter") + } + return MetricSeriesTags{key: remainder} +} + +func strategyEcdnSuccessError(remainder, rawID, realm, normalizedTenantID, key string) MetricSeriesTags { + // Operates on rawID: "2xx bdpx.host" (status class BEFORE the realm) + spaceIdx := strings.Index(rawID, " ") + if spaceIdx > 0 { + statusClass := rawID[:spaceIdx] + host := stripRealmPrefix(rawID[spaceIdx+1:], realm, normalizedTenantID) + return MetricSeriesTags{ + "statusClass": statusClass, + "host": host, + } + } + return MetricSeriesTags{"host": stripRealmPrefix(rawID, realm, normalizedTenantID)} +} + +func applyStrategy(strategy ExtractorStrategy, remainder, rawID, realm, normalizedTenantID, key string) MetricSeriesTags { + switch strategy { + case StrategyFamilyOrStatus: + return strategyFamilyOrStatus(remainder, rawID, realm, normalizedTenantID, key) + case StrategyFamilyOrOverallAgg: + return strategyFamilyOrOverallAgg(remainder, rawID, realm, normalizedTenantID, key) + case StrategyLastSpaceSplit: + return strategyLastSpaceSplit(remainder, rawID, realm, normalizedTenantID, key) + case StrategyLastDotSplit: + return strategyLastDotSplit(remainder, rawID, realm, normalizedTenantID, key) + case StrategyWholeAs: + return strategyWholeAs(remainder, rawID, realm, normalizedTenantID, key) + case StrategyEcdnSuccessError: + return strategyEcdnSuccessError(remainder, rawID, realm, normalizedTenantID, key) + default: + return MetricSeriesTags{} + } +} + +// ParseSeriesTagsParams holds parameters for ParseSeriesTags. +type ParseSeriesTagsParams struct { + Category string + MetricID string + SeriesID string + Context MetricsTagContext +} + +// ParseSeriesTags extracts the dimension tags for a single series id. +// +// Combines three tiers, most-authoritative last: +// 1. Request identity — realm/environment from the tenant id (never parsed from the series string). +// 2. String heuristics — category/metric-specific dimensions parsed from the packed id +// (apiFamily, host, cacheStatus, ...), or the raw remainder under "series" when no rule matches. +// 3. Applied filters — any filter that was sent with the request (MetricsTagContext) is stamped last, +// overriding a heuristic guess. +// +// The result is always a superset of the request context and never panics. +func ParseSeriesTags(params ParseSeriesTagsParams) MetricSeriesTags { + normalized := clients.NormalizeTenantID(params.Context.TenantID) + realm, environment := splitRealmEnvironment(params.Context.TenantID) + + tags := MetricSeriesTags{"realm": realm} + if environment != "" { + tags["environment"] = environment + } + + // Lookup rule by category+metricId (fallback to category+*) + var rule *ExtractorRule + if categoryRules, ok := extractorIndex[params.Category]; ok { + if r, ok := categoryRules[params.MetricID]; ok { + rule = &r + } else if r, ok := categoryRules["*"]; ok { + rule = &r + } + } + + remainder := stripRealmPrefix(params.SeriesID, realm, normalized) + + if remainder == params.MetricID { + // The series id is just the metric id echoed back (e.g. "cacheHitRate", + // "errors4xx") — a rollup/aggregate series carrying no per-series dimension. + // Don't run the extractor (which would mis-tag it as apiFamily/host/etc.) and + // don't record a "series" tag; identity tags alone are correct here. + tags["aggregation"] = "total" + } else if rule != nil { + // Apply strategy + dimensionTags := applyStrategy(rule.Strategy, remainder, params.SeriesID, realm, normalized, rule.Key) + for k, v := range dimensionTags { + tags[k] = v + } + } else if remainder != "" { + // No rule for this category/metric. Preserve the (realm-stripped) remainder + // so nothing is lost. + tags["series"] = remainder + } + + // Applied filters are authoritative — stamp them last so they override any heuristic guess + if params.Context.APIFamily != "" { + tags["apiFamily"] = params.Context.APIFamily + } + if params.Context.APIName != "" { + tags["apiName"] = params.Context.APIName + } + if params.Context.OcapiCategory != "" { + tags["ocapiCategory"] = params.Context.OcapiCategory + } + if params.Context.OcapiAPI != "" { + tags["ocapiApi"] = params.Context.OcapiAPI + } + if params.Context.ThirdPartyServiceID != "" { + tags["thirdPartyServiceId"] = params.Context.ThirdPartyServiceID + } + + return tags +} diff --git a/packages/b2c-tooling-sdk-go/operations/metrics/tags_golden_test.go b/packages/b2c-tooling-sdk-go/operations/metrics/tags_golden_test.go new file mode 100644 index 000000000..36fb1ad16 --- /dev/null +++ b/packages/b2c-tooling-sdk-go/operations/metrics/tags_golden_test.go @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +package metrics + +import ( + _ "embed" + "encoding/json" + "reflect" + "testing" +) + +//go:embed data/metrics-tags.golden.json +var goldenJSON []byte + +// GoldenTestCase represents a single test case from the golden fixture. +type GoldenTestCase struct { + Category string `json:"category"` + MetricID string `json:"metricId"` + SeriesID string `json:"seriesId"` + Context goldenContext `json:"context"` + ExpectedTags map[string]string `json:"expectedTags"` + Description string `json:"description,omitempty"` +} + +type goldenContext struct { + TenantID string `json:"tenantId"` + APIFamily string `json:"apiFamily,omitempty"` + APIName string `json:"apiName,omitempty"` + OcapiCategory string `json:"ocapiCategory,omitempty"` + OcapiAPI string `json:"ocapiApi,omitempty"` + ThirdPartyServiceID string `json:"thirdPartyServiceId,omitempty"` +} + +// GoldenFixture represents the complete golden test fixture. +type GoldenFixture struct { + Version string `json:"version"` + GeneratedAt string `json:"generatedAt"` + Description string `json:"description"` + TestCases []GoldenTestCase `json:"testCases"` +} + +func TestParseSeriesTags_Golden(t *testing.T) { + var fixture GoldenFixture + if err := json.Unmarshal(goldenJSON, &fixture); err != nil { + t.Fatalf("Failed to unmarshal golden fixture: %v", err) + } + + t.Logf("Running %d golden test cases from fixture version %s", len(fixture.TestCases), fixture.Version) + + for i, tc := range fixture.TestCases { + name := tc.Description + if name == "" { + name = tc.Category + "/" + tc.MetricID + "/" + tc.SeriesID + } + + t.Run(name, func(t *testing.T) { + got := ParseSeriesTags(ParseSeriesTagsParams{ + Category: tc.Category, + MetricID: tc.MetricID, + SeriesID: tc.SeriesID, + Context: MetricsTagContext{ + TenantID: tc.Context.TenantID, + APIFamily: tc.Context.APIFamily, + APIName: tc.Context.APIName, + OcapiCategory: tc.Context.OcapiCategory, + OcapiAPI: tc.Context.OcapiAPI, + ThirdPartyServiceID: tc.Context.ThirdPartyServiceID, + }, + }) + + if !reflect.DeepEqual(got, MetricSeriesTags(tc.ExpectedTags)) { + t.Errorf("Test case #%d (%s):\n got: %+v\n want: %+v", + i, tc.Description, got, tc.ExpectedTags) + } + }) + } +} + +func TestParseSeriesTags_EdgeCases(t *testing.T) { + tests := []struct { + name string + params ParseSeriesTagsParams + want MetricSeriesTags + }{ + { + name: "empty tenant id", + params: ParseSeriesTagsParams{ + Category: "scapi", + MetricID: "totalCalls", + SeriesID: "product", + Context: MetricsTagContext{TenantID: ""}, + }, + want: MetricSeriesTags{"realm": "", "apiFamily": "product"}, + }, + { + // A series whose id is just the metric id echoed back is a rollup with no + // per-series dimension → tagged aggregation=total (not apiFamily/series). + name: "series id equals metric id", + params: ParseSeriesTagsParams{ + Category: "mrt", + MetricID: "errorRate", + SeriesID: "errorRate", + Context: MetricsTagContext{TenantID: "bdpx_prd"}, + }, + want: MetricSeriesTags{"realm": "bdpx", "environment": "prd", "aggregation": "total"}, + }, + { + name: "unrecognized category", + params: ParseSeriesTagsParams{ + Category: "unknown", + MetricID: "metric", + SeriesID: "bdpx.value", + Context: MetricsTagContext{TenantID: "bdpx_prd"}, + }, + want: MetricSeriesTags{"realm": "bdpx", "environment": "prd", "series": "value"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ParseSeriesTags(tt.params) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseSeriesTags() = %+v, want %+v", got, tt.want) + } + }) + } +} diff --git a/packages/b2c-tooling-sdk-go/operations/metrics/window.go b/packages/b2c-tooling-sdk-go/operations/metrics/window.go new file mode 100644 index 000000000..34efc9ef6 --- /dev/null +++ b/packages/b2c-tooling-sdk-go/operations/metrics/window.go @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +package metrics + +import ( + "fmt" + "regexp" + "strconv" + "strings" + "time" +) + +const ( + // MetricsRetentionPeriod is how far back the Metrics API retains data (30 days). + // from must be no older than serverNow - 30 days, or the API returns 400. + MetricsRetentionPeriod = 30 * 24 * time.Hour + + // MetricsDefaultWindow is the default (and maximum) width of a metrics time window (24 hours). + MetricsDefaultWindow = 24 * time.Hour + + // MetricsRetentionSafetyMargin is a safety margin kept inside the retention window when clamping from. + // 5 minutes comfortably covers latency and typical skew. + MetricsRetentionSafetyMargin = 5 * time.Minute +) + +// ResolvedMetricsWindow represents a fully resolved time window with both bounds present. +type ResolvedMetricsWindow struct { + From time.Time + To time.Time + FromEpochSeconds int64 + ToEpochSeconds int64 + ClampedFrom bool // True if from was clamped forward to stay inside retention + DefaultedWindow bool // True if a bound was derived from the 24-hour default window +} + +// parseRelativeTime parses a relative duration string like "5m", "1h", "30d" into a duration. +// Returns 0 and an error if the string is invalid. +var relativeTimeRegex = regexp.MustCompile(`^(\d+)(s|m|h|d)$`) + +func parseRelativeTime(s string) (time.Duration, error) { + matches := relativeTimeRegex.FindStringSubmatch(strings.TrimSpace(s)) + if matches == nil { + return 0, fmt.Errorf("invalid relative time format: %q (expected format: 5m, 1h, 2d)", s) + } + + value, err := strconv.ParseInt(matches[1], 10, 64) + if err != nil { + return 0, fmt.Errorf("invalid number in relative time: %q", matches[1]) + } + + unit := matches[2] + switch unit { + case "s": + return time.Duration(value) * time.Second, nil + case "m": + return time.Duration(value) * time.Minute, nil + case "h": + return time.Duration(value) * time.Hour, nil + case "d": + return time.Duration(value) * 24 * time.Hour, nil + default: + return 0, fmt.Errorf("unknown time unit: %q", unit) + } +} + +// parseSinceTime parses a time bound that can be: +// - A relative duration like "5m", "1h", "2d" (interpreted as "ago" from now) +// - An ISO 8601 timestamp string +func parseSinceTime(s string, now time.Time) (time.Time, error) { + s = strings.TrimSpace(s) + + // Try relative time first + if duration, err := parseRelativeTime(s); err == nil { + return now.Add(-duration), nil + } + + // Try parsing as ISO 8601 timestamp + // Try multiple formats + formats := []string{ + time.RFC3339, + time.RFC3339Nano, + "2006-01-02T15:04:05Z", + "2006-01-02T15:04:05", + "2006-01-02", + } + + for _, format := range formats { + if t, err := time.Parse(format, s); err == nil { + return t, nil + } + } + + return time.Time{}, fmt.Errorf("invalid time format: %q (expected relative like '5m' or ISO 8601)", s) +} + +// ParseMetricsBound parses a single metrics time bound (from or to) into a time.Time. +// Accepts: +// - time.Time (returned as-is) +// - Unix epoch milliseconds (int64) +// - Relative duration string like "5m", "1h", "2d" (relative to now) +// - ISO 8601 timestamp string +func ParseMetricsBound(value interface{}, now time.Time) (time.Time, error) { + switch v := value.(type) { + case time.Time: + return v, nil + case int64: + // Epoch milliseconds + return time.UnixMilli(v), nil + case string: + return parseSinceTime(v, now) + default: + return time.Time{}, fmt.Errorf("invalid bound type: %T (expected time.Time, int64, or string)", value) + } +} + +// MetricsWindowInput holds raw from/to/window inputs before resolution. +type MetricsWindowInput struct { + From interface{} // time.Time, int64 (epoch ms), or string (relative/ISO) + To interface{} // time.Time, int64 (epoch ms), or string (relative/ISO) + Window interface{} // time.Duration or string (relative) +} + +// ResolveMetricsWindow resolves from/to/window inputs into concrete bounds for the Metrics API. +// +// Resolution rules: +// - from + to: used as given; window must NOT also be set +// - from + window: to = from + window +// - to + window: from = to - window +// - window only: the last window (to = now, from = now - window) +// - from only: 24-hour window forward from from (to = min(from + 24h, now)) +// - to only: 24-hour window back from to (from = to - 24h) +// - nothing: the last 24 hours (to = now, from = now - 24h) +// +// A from that predates the 30-day retention floor is clamped forward to stay within retention. +// The clamp is applied before deriving the companion bound so the window width is preserved. +func ResolveMetricsWindow(input MetricsWindowInput, now time.Time) (*ResolvedMetricsWindow, error) { + hasFrom := input.From != nil + hasTo := input.To != nil + hasWindow := input.Window != nil + + if hasFrom && hasTo && hasWindow { + return nil, fmt.Errorf("specify at most two of from, to, and window — not all three") + } + + var from, to time.Time + var err error + clampedFrom := false + defaultedWindow := false + + // Clamp function for retention enforcement + earliestSafe := now.Add(-MetricsRetentionPeriod + MetricsRetentionSafetyMargin) + clampFrom := func(t time.Time) time.Time { + if t.Before(earliestSafe) { + clampedFrom = true + return earliestSafe + } + return t + } + + // Parse window if provided + var windowDuration time.Duration + if hasWindow { + switch w := input.Window.(type) { + case time.Duration: + windowDuration = w + case string: + windowDuration, err = parseRelativeTime(w) + if err != nil { + return nil, fmt.Errorf("invalid window: %w", err) + } + default: + return nil, fmt.Errorf("invalid window type: %T (expected time.Duration or string)", input.Window) + } + } + + // Parse from and to if provided + if hasFrom { + from, err = ParseMetricsBound(input.From, now) + if err != nil { + return nil, fmt.Errorf("invalid from: %w", err) + } + from = clampFrom(from) + } + + if hasTo { + to, err = ParseMetricsBound(input.To, now) + if err != nil { + return nil, fmt.Errorf("invalid to: %w", err) + } + } + + // Apply resolution rules + if hasWindow { + if hasFrom && !hasTo { + to = from.Add(windowDuration) + } else if hasTo && !hasFrom { + from = to.Add(-windowDuration) + } else { + // window alone → the last {window} + to = now + from = now.Add(-windowDuration) + } + } else if !(hasFrom && hasTo) { + // No explicit window and at least one bound open → fill from 24-hour default + defaultedWindow = true + if hasFrom && !hasTo { + // A window forward from from, but never past now + to = from.Add(MetricsDefaultWindow) + if to.After(now) { + to = now + } + } else if hasTo && !hasFrom { + from = to.Add(-MetricsDefaultWindow) + } else { + // Nothing supplied → the last 24 hours + to = now + from = now.Add(-MetricsDefaultWindow) + } + } + + // Clamp from again if it was derived from to + from = clampFrom(from) + + if from.After(to) { + return nil, fmt.Errorf("invalid time window: from (%s) must be before to (%s)", from.Format(time.RFC3339), to.Format(time.RFC3339)) + } + + return &ResolvedMetricsWindow{ + From: from, + To: to, + FromEpochSeconds: from.Unix(), + ToEpochSeconds: to.Unix(), + ClampedFrom: clampedFrom, + DefaultedWindow: defaultedWindow, + }, nil +} diff --git a/packages/b2c-tooling-sdk-go/operations/metrics/window_test.go b/packages/b2c-tooling-sdk-go/operations/metrics/window_test.go new file mode 100644 index 000000000..f85ed93c9 --- /dev/null +++ b/packages/b2c-tooling-sdk-go/operations/metrics/window_test.go @@ -0,0 +1,294 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +package metrics + +import ( + "testing" + "time" +) + +func TestParseRelativeTime(t *testing.T) { + tests := []struct { + name string + input string + want time.Duration + wantErr bool + }{ + { + name: "seconds", + input: "30s", + want: 30 * time.Second, + }, + { + name: "minutes", + input: "5m", + want: 5 * time.Minute, + }, + { + name: "hours", + input: "2h", + want: 2 * time.Hour, + }, + { + name: "days", + input: "7d", + want: 7 * 24 * time.Hour, + }, + { + name: "invalid format", + input: "5", + wantErr: true, + }, + { + name: "invalid unit", + input: "5x", + wantErr: true, + }, + { + name: "empty string", + input: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseRelativeTime(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("parseRelativeTime(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) + return + } + if !tt.wantErr && got != tt.want { + t.Errorf("parseRelativeTime(%q) = %v, want %v", tt.input, got, tt.want) + } + }) + } +} + +func TestParseSinceTime(t *testing.T) { + now := time.Date(2026, 7, 14, 12, 0, 0, 0, time.UTC) + + tests := []struct { + name string + input string + want time.Time + wantErr bool + }{ + { + name: "relative 5 minutes ago", + input: "5m", + want: now.Add(-5 * time.Minute), + }, + { + name: "relative 1 hour ago", + input: "1h", + want: now.Add(-1 * time.Hour), + }, + { + name: "relative 7 days ago", + input: "7d", + want: now.Add(-7 * 24 * time.Hour), + }, + { + name: "ISO 8601 RFC3339", + input: "2026-07-14T10:00:00Z", + want: time.Date(2026, 7, 14, 10, 0, 0, 0, time.UTC), + }, + { + name: "ISO 8601 date only", + input: "2026-07-14", + want: time.Date(2026, 7, 14, 0, 0, 0, 0, time.UTC), + }, + { + name: "invalid format", + input: "not a time", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseSinceTime(tt.input, now) + if (err != nil) != tt.wantErr { + t.Errorf("parseSinceTime(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) + return + } + if !tt.wantErr && !got.Equal(tt.want) { + t.Errorf("parseSinceTime(%q) = %v, want %v", tt.input, got, tt.want) + } + }) + } +} + +func TestResolveMetricsWindow(t *testing.T) { + now := time.Date(2026, 7, 14, 12, 0, 0, 0, time.UTC) + + tests := []struct { + name string + input MetricsWindowInput + wantFromOffset time.Duration // Offset from now + wantToOffset time.Duration // Offset from now + wantClampedFrom bool + wantDefaulted bool + wantErr bool + }{ + { + name: "from and to provided", + input: MetricsWindowInput{ + From: now.Add(-2 * time.Hour), + To: now.Add(-1 * time.Hour), + }, + wantFromOffset: -2 * time.Hour, + wantToOffset: -1 * time.Hour, + }, + { + name: "from and window", + input: MetricsWindowInput{ + From: now.Add(-7 * 24 * time.Hour), + Window: 1 * time.Hour, + }, + wantFromOffset: -7 * 24 * time.Hour, + wantToOffset: -7*24*time.Hour + 1*time.Hour, + }, + { + name: "to and window", + input: MetricsWindowInput{ + To: now, + Window: 2 * time.Hour, + }, + wantFromOffset: -2 * time.Hour, + wantToOffset: 0, + }, + { + name: "window only", + input: MetricsWindowInput{ + Window: 6 * time.Hour, + }, + wantFromOffset: -6 * time.Hour, + wantToOffset: 0, + }, + { + name: "from only (defaulted 24h window)", + input: MetricsWindowInput{ + From: now.Add(-2 * time.Hour), + }, + wantFromOffset: -2 * time.Hour, + wantToOffset: 0, // min(from + 24h, now) = now (capped) + wantDefaulted: true, + }, + { + name: "to only (defaulted 24h window)", + input: MetricsWindowInput{ + To: now, + }, + wantFromOffset: -24 * time.Hour, + wantToOffset: 0, + wantDefaulted: true, + }, + { + name: "nothing (default last 24h)", + input: MetricsWindowInput{}, + wantFromOffset: -24 * time.Hour, + wantToOffset: 0, + wantDefaulted: true, + }, + { + name: "from before retention floor (clamped)", + input: MetricsWindowInput{ + From: now.Add(-31 * 24 * time.Hour), // Older than 30 days + To: now, + }, + wantFromOffset: -MetricsRetentionPeriod + MetricsRetentionSafetyMargin, + wantToOffset: 0, + wantClampedFrom: true, + }, + { + name: "all three specified (error)", + input: MetricsWindowInput{ + From: now.Add(-2 * time.Hour), + To: now, + Window: 1 * time.Hour, + }, + wantErr: true, + }, + { + name: "from after to (error)", + input: MetricsWindowInput{ + From: now, + To: now.Add(-1 * time.Hour), + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ResolveMetricsWindow(tt.input, now) + if (err != nil) != tt.wantErr { + t.Errorf("ResolveMetricsWindow() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if tt.wantErr { + return + } + + wantFrom := now.Add(tt.wantFromOffset) + wantTo := now.Add(tt.wantToOffset) + + if !got.From.Equal(wantFrom) { + t.Errorf("From = %v, want %v (offset %v from now)", got.From, wantFrom, tt.wantFromOffset) + } + + if !got.To.Equal(wantTo) { + t.Errorf("To = %v, want %v (offset %v from now)", got.To, wantTo, tt.wantToOffset) + } + + if got.ClampedFrom != tt.wantClampedFrom { + t.Errorf("ClampedFrom = %v, want %v", got.ClampedFrom, tt.wantClampedFrom) + } + + if got.DefaultedWindow != tt.wantDefaulted { + t.Errorf("DefaultedWindow = %v, want %v", got.DefaultedWindow, tt.wantDefaulted) + } + + // Verify epoch seconds match + if got.FromEpochSeconds != got.From.Unix() { + t.Errorf("FromEpochSeconds = %d, want %d", got.FromEpochSeconds, got.From.Unix()) + } + + if got.ToEpochSeconds != got.To.Unix() { + t.Errorf("ToEpochSeconds = %d, want %d", got.ToEpochSeconds, got.To.Unix()) + } + }) + } +} + +func TestResolveMetricsWindow_StringInputs(t *testing.T) { + now := time.Date(2026, 7, 14, 12, 0, 0, 0, time.UTC) + + input := MetricsWindowInput{ + From: "7d", // 7 days ago + Window: "1h", // 1 hour window + } + + got, err := ResolveMetricsWindow(input, now) + if err != nil { + t.Fatalf("ResolveMetricsWindow() unexpected error: %v", err) + } + + wantFrom := now.Add(-7 * 24 * time.Hour) + wantTo := wantFrom.Add(1 * time.Hour) + + if !got.From.Equal(wantFrom) { + t.Errorf("From = %v, want %v", got.From, wantFrom) + } + + if !got.To.Equal(wantTo) { + t.Errorf("To = %v, want %v", got.To, wantTo) + } +} diff --git a/packages/b2c-tooling-sdk/package.json b/packages/b2c-tooling-sdk/package.json index 0b7d23062..417346fdf 100644 --- a/packages/b2c-tooling-sdk/package.json +++ b/packages/b2c-tooling-sdk/package.json @@ -241,7 +241,9 @@ "generate:guides-index": "tsx scripts/generate-guides-index.ts", "generate:tooling-index": "tsx scripts/generate-tooling-index.ts", "generate:help-corpus": "tsx scripts/generate-help-corpus.ts", - "enrich:docs": "tsx scripts/enrich-docs.ts" + "enrich:docs": "tsx scripts/enrich-docs.ts", + "generate:metrics-tags-catalog": "tsx scripts/generate-metrics-tags-catalog.ts", + "generate:metrics-tags-golden": "tsx scripts/generate-metrics-tags-golden.ts" }, "devDependencies": { "@eslint/compat": "catalog:", diff --git a/packages/b2c-tooling-sdk/scripts/generate-metrics-tags-catalog.ts b/packages/b2c-tooling-sdk/scripts/generate-metrics-tags-catalog.ts new file mode 100644 index 000000000..e65963521 --- /dev/null +++ b/packages/b2c-tooling-sdk/scripts/generate-metrics-tags-catalog.ts @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +/** + * Generates the metrics tags extraction catalog as JSON. + * + * This declarative catalog defines how series ids are parsed into dimension + * tags. It is consumed by the Go Grafana plugin to replicate the TypeScript + * parsing logic server-side. + * + * Run with: pnpm --filter @salesforce/b2c-tooling-sdk run generate:metrics-tags-catalog + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import {fileURLToPath} from 'node:url'; +import {EXTRACTOR_CATALOG} from '../src/operations/metrics/tags.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const OUTPUT_PATH = path.resolve(__dirname, '../specs/metrics-tags-catalog.json'); + +interface CatalogOutput { + version: string; + generatedAt: string; + description: string; + strategies: string[]; + rules: typeof EXTRACTOR_CATALOG; +} + +async function generateCatalog(): Promise { + const catalog: CatalogOutput = { + version: '1.0.0', + generatedAt: new Date().toISOString(), + description: + 'Declarative catalog of metrics series tag extraction rules. Defines how packed series ids are parsed into structured dimension tags.', + strategies: [ + 'familyOrStatus', + 'familyOrOverallAgg', + 'lastSpaceSplit', + 'lastDotSplit', + 'wholeAs', + 'ecdnSuccessError', + ], + rules: EXTRACTOR_CATALOG, + }; + + fs.writeFileSync(OUTPUT_PATH, JSON.stringify(catalog, null, 2) + '\n'); + + console.log(`Generated metrics tags catalog with ${catalog.rules.length} rules at ${OUTPUT_PATH}`); +} + +generateCatalog().catch((err) => { + console.error('Failed to generate metrics tags catalog:', err); + process.exit(1); +}); diff --git a/packages/b2c-tooling-sdk/scripts/generate-metrics-tags-golden.ts b/packages/b2c-tooling-sdk/scripts/generate-metrics-tags-golden.ts new file mode 100644 index 000000000..16666b235 --- /dev/null +++ b/packages/b2c-tooling-sdk/scripts/generate-metrics-tags-golden.ts @@ -0,0 +1,361 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +/** + * Generates the golden test fixture for metrics tags extraction. + * + * This fixture covers every extraction strategy with representative inputs, + * including edge cases. Both TypeScript and Go tests assert against it to + * guarantee parity. + * + * Run with: pnpm --filter @salesforce/b2c-tooling-sdk run generate:metrics-tags-golden + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import {fileURLToPath} from 'node:url'; +import type {MetricCategory} from '../src/operations/metrics/index.js'; +import {parseSeriesTags, type MetricsTagContext, type MetricSeriesTags} from '../src/operations/metrics/tags.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const OUTPUT_PATH = path.resolve(__dirname, '../specs/metrics-tags.golden.json'); + +interface GoldenTestCase { + category: MetricCategory; + metricId: string; + seriesId: string; + context: MetricsTagContext; + expectedTags: MetricSeriesTags; + description?: string; +} + +interface GoldenFixture { + version: string; + generatedAt: string; + description: string; + testCases: GoldenTestCase[]; +} + +/** + * Comprehensive test matrix covering every strategy and edge case. + * Each case runs through parseSeriesTags to capture the expected output. + */ +const TEST_CASES: Array> = [ + // Request-derived identity tags + { + category: 'scapi', + metricId: 'totalCalls', + seriesId: 'bdpx.product', + context: {tenantId: 'f_ecom_bdpx_prd'}, + description: 'Derives realm and environment from prefixed org id', + }, + { + category: 'scapi', + metricId: 'totalCalls', + seriesId: 'bdpx.product', + context: {tenantId: 'bdpx_prd'}, + description: 'Derives realm and environment from bare tenant id', + }, + { + category: 'overall', + metricId: 'requests', + seriesId: 'acme Requests', + context: {tenantId: 'acme'}, + description: 'Realm with no environment (no underscore)', + }, + { + category: 'scapi', + metricId: 'totalCalls', + seriesId: 'multi_underscore_realm_env.product', + context: {tenantId: 'multi_underscore_realm_env'}, + description: 'Multi-segment tenant id (environment is last segment)', + }, + + // SCAPI familyOrStatus strategy + { + category: 'scapi', + metricId: 'totalCalls', + seriesId: 'bdpx.product', + context: {tenantId: 'bdpx_prd'}, + description: 'SCAPI totalCalls: apiFamily', + }, + { + category: 'scapi', + metricId: 'responseCount', + seriesId: 'bdpx 2xx', + context: {tenantId: 'bdpx_prd'}, + description: 'SCAPI responseCount: statusClass (not apiFamily)', + }, + { + category: 'scapi', + metricId: 'responseCount', + seriesId: 'bdpx 4xx', + context: {tenantId: 'bdpx_prd'}, + description: 'SCAPI responseCount: statusClass 4xx', + }, + + // SCAPI familyOrOverallAgg strategy + { + category: 'scapi', + metricId: 'requestLatency', + seriesId: 'bdpx Average overall latency', + context: {tenantId: 'bdpx_prd'}, + description: 'SCAPI requestLatency: overall aggregation', + }, + { + category: 'scapi', + metricId: 'requestLatency', + seriesId: 'bdpx.product', + context: {tenantId: 'bdpx_prd'}, + description: 'SCAPI requestLatency: apiFamily (not overall)', + }, + + // SCAPI drill-down ids (returned when filtering by apiFamily) — real bdpx_prd forms. + // The leading family + trailing vN version split off; the middle is the api name. + { + category: 'scapi', + metricId: 'totalCalls', + seriesId: 'bdpx.shopper.auth.v1', + context: {tenantId: 'bdpx_prd', apiFamily: 'shopper'}, + description: 'SCAPI drill-down: apiFamily + apiName + apiVersion (filter overrides family)', + }, + { + category: 'scapi', + metricId: 'requestLatency', + seriesId: 'bdpx.search.shopper-search.v1', + context: {tenantId: 'bdpx_prd', apiFamily: 'search'}, + description: 'SCAPI drill-down: hyphenated apiName + version', + }, + { + category: 'scapi', + metricId: 'totalCalls', + seriesId: 'bdpx.shopper.baskets.v2', + context: {tenantId: 'bdpx_prd'}, + description: 'SCAPI drill-down without filter: family/name/version all from id', + }, + + // Rollup series whose id is the bare metric id echoed back (real bdpx_prd forms + // under a family filter): must be tagged aggregation=total, never apiFamily/series. + { + category: 'scapi', + metricId: 'cacheHitRate', + seriesId: 'cacheHitRate', + context: {tenantId: 'bdpx_prd', apiFamily: 'shopper'}, + description: 'SCAPI rollup: cacheHitRate id echoes metric id → aggregation=total (filter still applies)', + }, + { + category: 'scapi', + metricId: 'errors4xx', + seriesId: 'errors4xx', + context: {tenantId: 'bdpx_prd'}, + description: 'SCAPI rollup: errors4xx id echoes metric id → aggregation=total (not apiFamily)', + }, + + // SCAPI lastSpaceSplit strategy (cacheHitRate) + { + category: 'scapi', + metricId: 'cacheHitRate', + seriesId: 'bdpx.product HIT', + context: {tenantId: 'bdpx_prd'}, + description: 'SCAPI cacheHitRate: apiFamily + cacheStatus split on space', + }, + { + category: 'scapi', + metricId: 'cacheHitRate', + seriesId: 'bdpx.custom MISS', + context: {tenantId: 'bdpx_prd'}, + description: 'SCAPI cacheHitRate: MISS status', + }, + { + category: 'scapi', + metricId: 'cacheHitRate', + seriesId: 'bdpx.product', + context: {tenantId: 'bdpx_prd'}, + description: 'SCAPI cacheHitRate: no space (apiFamily only)', + }, + + // SCAPI wholeAs strategy (errors4xx) + { + category: 'scapi', + metricId: 'errors4xx', + seriesId: 'bdpx.shopper', + context: {tenantId: 'bdpx_prd'}, + description: 'SCAPI errors4xx: whole remainder as apiFamily', + }, + + // eCDN ecdnSuccessError strategy + { + category: 'ecdn', + metricId: 'successAndError', + seriesId: '2xx bdpx.bdpx-prod_cc-bm_net', + context: {tenantId: 'bdpx_prd'}, + description: 'eCDN successAndError: status class before realm', + }, + { + category: 'ecdn', + metricId: 'successAndError', + seriesId: '5xx bdpx.bdpx-stg_host', + context: {tenantId: 'bdpx_stg'}, + description: 'eCDN successAndError: 5xx status', + }, + { + category: 'ecdn', + metricId: 'successAndError', + seriesId: 'bdpx.bdpx-prod_host', + context: {tenantId: 'bdpx_prd'}, + description: 'eCDN successAndError: no status (host only)', + }, + + // eCDN wildcard (wholeAs host) + { + category: 'ecdn', + metricId: 'totalRequests', + seriesId: 'bdpx.bdpx-prod_cc-bm_net', + context: {tenantId: 'bdpx_prd'}, + description: 'eCDN other metrics: host only', + }, + + // third-party wholeAs (host) + { + category: 'third-party', + metricId: 'callsCount', + seriesId: 'bdpx.login.salesforce.com', + context: {tenantId: 'bdpx_prd'}, + description: 'third-party callsCount: dotted host as whole remainder', + }, + { + category: 'third-party', + metricId: 'callsP95', + seriesId: 'bdpx.api.example.com', + context: {tenantId: 'bdpx_prd'}, + description: 'third-party callsP95: dotted host', + }, + + // third-party lastDotSplit (remoteExceptions) + { + category: 'third-party', + metricId: 'remoteExceptions', + seriesId: 'bdpx.xitgmcd3.api.commercecloud.salesforce.com.socketReadTimeout', + context: {tenantId: 'bdpx_prd'}, + description: 'third-party remoteExceptions: host + exceptionType (last dot split)', + }, + { + category: 'third-party', + metricId: 'remoteExceptions', + seriesId: 'bdpx.login.salesforce.com.connectionTimeout', + context: {tenantId: 'bdpx_prd'}, + description: 'third-party remoteExceptions: another exception type', + }, + { + category: 'third-party', + metricId: 'remoteExceptions', + seriesId: 'bdpx.host', + context: {tenantId: 'bdpx_prd'}, + description: 'third-party remoteExceptions: no dot (host only)', + }, + + // OCAPI wholeAs (ocapiCategory) + { + category: 'ocapi', + metricId: 'totalCalls', + seriesId: 'bdpx.shop', + context: {tenantId: 'bdpx_prd'}, + description: 'OCAPI totalCalls: ocapiCategory', + }, + { + category: 'ocapi', + metricId: 'callsMean', + seriesId: 'bdpx.data', + context: {tenantId: 'bdpx_prd'}, + description: 'OCAPI callsMean: ocapiCategory', + }, + + // Controller wildcard (wholeAs controller) + { + category: 'controller', + metricId: 'callsMean', + seriesId: 'bdpx.Checkout-Begin', + context: {tenantId: 'bdpx_prd'}, + description: 'Controller wildcard: controller name', + }, + { + category: 'controller', + metricId: 'totalCalls', + seriesId: 'bdpx.Home-Show', + context: {tenantId: 'bdpx_prd'}, + description: 'Controller wildcard: applies to any controller metric', + }, + + // Applied filters override heuristics + { + category: 'scapi', + metricId: 'totalCalls', + seriesId: 'bdpx.shopper.auth.v1', + context: {tenantId: 'bdpx_prd', apiFamily: 'shopper'}, + description: 'Applied apiFamily filter overrides drill-down id', + }, + { + category: 'scapi', + metricId: 'totalCalls', + seriesId: 'totalCalls', + context: {tenantId: 'bdpx_prd', apiFamily: 'products', apiName: 'shopper-products'}, + description: 'Applied apiFamily + apiName filters', + }, + { + category: 'ocapi', + metricId: 'totalCalls', + seriesId: 'totalCalls', + context: {tenantId: 'bdpx_prd', ocapiCategory: 'shop'}, + description: 'Applied ocapiCategory filter', + }, + { + category: 'third-party', + metricId: 'callsCount', + seriesId: 'bdpx.login.salesforce.com', + context: {tenantId: 'bdpx_prd', thirdPartyServiceId: 'my.svc'}, + description: 'Applied thirdPartyServiceId filter (heuristic host still present)', + }, + + // Fallback behavior + { + category: 'overall', + metricId: 'requests', + seriesId: 'bdpx Requests', + context: {tenantId: 'bdpx_prd'}, + description: 'Unrecognized pattern: prose label under series tag', + }, + { + category: 'mrt', + metricId: 'errorRate', + seriesId: 'errorRate', + context: {tenantId: 'bdpx_prd'}, + description: 'Fallback series echoes metric id (not captured as series tag)', + }, +]; + +async function generateGolden(): Promise { + const testCases: GoldenTestCase[] = TEST_CASES.map((tc) => ({ + ...tc, + expectedTags: parseSeriesTags(tc), + })); + + const fixture: GoldenFixture = { + version: '1.0.0', + generatedAt: new Date().toISOString(), + description: + 'Golden test fixture for metrics tags extraction. Covers all strategies and edge cases. Both TS and Go tests assert against this.', + testCases, + }; + + fs.writeFileSync(OUTPUT_PATH, JSON.stringify(fixture, null, 2) + '\n'); + + console.log(`Generated metrics tags golden fixture with ${testCases.length} test cases at ${OUTPUT_PATH}`); +} + +generateGolden().catch((err) => { + console.error('Failed to generate metrics tags golden fixture:', err); + process.exit(1); +}); diff --git a/packages/b2c-tooling-sdk/specs/metrics-tags-catalog.json b/packages/b2c-tooling-sdk/specs/metrics-tags-catalog.json new file mode 100644 index 000000000..24ff9b14a --- /dev/null +++ b/packages/b2c-tooling-sdk/specs/metrics-tags-catalog.json @@ -0,0 +1,87 @@ +{ + "version": "1.0.0", + "generatedAt": "2026-07-14T18:47:24.331Z", + "description": "Declarative catalog of metrics series tag extraction rules. Defines how packed series ids are parsed into structured dimension tags.", + "strategies": [ + "familyOrStatus", + "familyOrOverallAgg", + "lastSpaceSplit", + "lastDotSplit", + "wholeAs", + "ecdnSuccessError" + ], + "rules": [ + { + "category": "scapi", + "metricId": "totalCalls", + "strategy": "familyOrStatus" + }, + { + "category": "scapi", + "metricId": "requestLatency", + "strategy": "familyOrOverallAgg" + }, + { + "category": "scapi", + "metricId": "responseCount", + "strategy": "familyOrStatus" + }, + { + "category": "scapi", + "metricId": "errors4xx", + "strategy": "wholeAs", + "key": "apiFamily" + }, + { + "category": "scapi", + "metricId": "cacheHitRate", + "strategy": "lastSpaceSplit" + }, + { + "category": "ocapi", + "metricId": "totalCalls", + "strategy": "wholeAs", + "key": "ocapiCategory" + }, + { + "category": "ocapi", + "metricId": "callsMean", + "strategy": "wholeAs", + "key": "ocapiCategory" + }, + { + "category": "controller", + "metricId": "*", + "strategy": "wholeAs", + "key": "controller" + }, + { + "category": "third-party", + "metricId": "callsCount", + "strategy": "wholeAs", + "key": "host" + }, + { + "category": "third-party", + "metricId": "callsP95", + "strategy": "wholeAs", + "key": "host" + }, + { + "category": "third-party", + "metricId": "remoteExceptions", + "strategy": "lastDotSplit" + }, + { + "category": "ecdn", + "metricId": "successAndError", + "strategy": "ecdnSuccessError" + }, + { + "category": "ecdn", + "metricId": "*", + "strategy": "wholeAs", + "key": "host" + } + ] +} diff --git a/packages/b2c-tooling-sdk/specs/metrics-tags.golden.json b/packages/b2c-tooling-sdk/specs/metrics-tags.golden.json new file mode 100644 index 000000000..5ecd0308d --- /dev/null +++ b/packages/b2c-tooling-sdk/specs/metrics-tags.golden.json @@ -0,0 +1,552 @@ +{ + "version": "1.0.0", + "generatedAt": "2026-07-14T18:48:22.984Z", + "description": "Golden test fixture for metrics tags extraction. Covers all strategies and edge cases. Both TS and Go tests assert against this.", + "testCases": [ + { + "category": "scapi", + "metricId": "totalCalls", + "seriesId": "bdpx.product", + "context": { + "tenantId": "f_ecom_bdpx_prd" + }, + "description": "Derives realm and environment from prefixed org id", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "apiFamily": "product" + } + }, + { + "category": "scapi", + "metricId": "totalCalls", + "seriesId": "bdpx.product", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "Derives realm and environment from bare tenant id", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "apiFamily": "product" + } + }, + { + "category": "overall", + "metricId": "requests", + "seriesId": "acme Requests", + "context": { + "tenantId": "acme" + }, + "description": "Realm with no environment (no underscore)", + "expectedTags": { + "realm": "acme", + "series": "Requests" + } + }, + { + "category": "scapi", + "metricId": "totalCalls", + "seriesId": "multi_underscore_realm_env.product", + "context": { + "tenantId": "multi_underscore_realm_env" + }, + "description": "Multi-segment tenant id (environment is last segment)", + "expectedTags": { + "realm": "multi_underscore_realm", + "environment": "env", + "apiFamily": "multi_underscore_realm_env", + "apiName": "product" + } + }, + { + "category": "scapi", + "metricId": "totalCalls", + "seriesId": "bdpx.product", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "SCAPI totalCalls: apiFamily", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "apiFamily": "product" + } + }, + { + "category": "scapi", + "metricId": "responseCount", + "seriesId": "bdpx 2xx", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "SCAPI responseCount: statusClass (not apiFamily)", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "statusClass": "2xx" + } + }, + { + "category": "scapi", + "metricId": "responseCount", + "seriesId": "bdpx 4xx", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "SCAPI responseCount: statusClass 4xx", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "statusClass": "4xx" + } + }, + { + "category": "scapi", + "metricId": "requestLatency", + "seriesId": "bdpx Average overall latency", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "SCAPI requestLatency: overall aggregation", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "aggregation": "overall" + } + }, + { + "category": "scapi", + "metricId": "requestLatency", + "seriesId": "bdpx.product", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "SCAPI requestLatency: apiFamily (not overall)", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "apiFamily": "product" + } + }, + { + "category": "scapi", + "metricId": "totalCalls", + "seriesId": "bdpx.shopper.auth.v1", + "context": { + "tenantId": "bdpx_prd", + "apiFamily": "shopper" + }, + "description": "SCAPI drill-down: apiFamily + apiName + apiVersion (filter overrides family)", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "apiFamily": "shopper", + "apiVersion": "v1", + "apiName": "auth" + } + }, + { + "category": "scapi", + "metricId": "requestLatency", + "seriesId": "bdpx.search.shopper-search.v1", + "context": { + "tenantId": "bdpx_prd", + "apiFamily": "search" + }, + "description": "SCAPI drill-down: hyphenated apiName + version", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "apiFamily": "search", + "apiVersion": "v1", + "apiName": "shopper-search" + } + }, + { + "category": "scapi", + "metricId": "totalCalls", + "seriesId": "bdpx.shopper.baskets.v2", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "SCAPI drill-down without filter: family/name/version all from id", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "apiFamily": "shopper", + "apiVersion": "v2", + "apiName": "baskets" + } + }, + { + "category": "scapi", + "metricId": "cacheHitRate", + "seriesId": "cacheHitRate", + "context": { + "tenantId": "bdpx_prd", + "apiFamily": "shopper" + }, + "description": "SCAPI rollup: cacheHitRate id echoes metric id → aggregation=total (filter still applies)", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "aggregation": "total", + "apiFamily": "shopper" + } + }, + { + "category": "scapi", + "metricId": "errors4xx", + "seriesId": "errors4xx", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "SCAPI rollup: errors4xx id echoes metric id → aggregation=total (not apiFamily)", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "aggregation": "total" + } + }, + { + "category": "scapi", + "metricId": "cacheHitRate", + "seriesId": "bdpx.product HIT", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "SCAPI cacheHitRate: apiFamily + cacheStatus split on space", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "apiFamily": "product", + "cacheStatus": "HIT" + } + }, + { + "category": "scapi", + "metricId": "cacheHitRate", + "seriesId": "bdpx.custom MISS", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "SCAPI cacheHitRate: MISS status", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "apiFamily": "custom", + "cacheStatus": "MISS" + } + }, + { + "category": "scapi", + "metricId": "cacheHitRate", + "seriesId": "bdpx.product", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "SCAPI cacheHitRate: no space (apiFamily only)", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "apiFamily": "product" + } + }, + { + "category": "scapi", + "metricId": "errors4xx", + "seriesId": "bdpx.shopper", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "SCAPI errors4xx: whole remainder as apiFamily", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "apiFamily": "shopper" + } + }, + { + "category": "ecdn", + "metricId": "successAndError", + "seriesId": "2xx bdpx.bdpx-prod_cc-bm_net", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "eCDN successAndError: status class before realm", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "statusClass": "2xx", + "host": "bdpx-prod_cc-bm_net" + } + }, + { + "category": "ecdn", + "metricId": "successAndError", + "seriesId": "5xx bdpx.bdpx-stg_host", + "context": { + "tenantId": "bdpx_stg" + }, + "description": "eCDN successAndError: 5xx status", + "expectedTags": { + "realm": "bdpx", + "environment": "stg", + "statusClass": "5xx", + "host": "bdpx-stg_host" + } + }, + { + "category": "ecdn", + "metricId": "successAndError", + "seriesId": "bdpx.bdpx-prod_host", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "eCDN successAndError: no status (host only)", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "host": "bdpx-prod_host" + } + }, + { + "category": "ecdn", + "metricId": "totalRequests", + "seriesId": "bdpx.bdpx-prod_cc-bm_net", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "eCDN other metrics: host only", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "host": "bdpx-prod_cc-bm_net" + } + }, + { + "category": "third-party", + "metricId": "callsCount", + "seriesId": "bdpx.login.salesforce.com", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "third-party callsCount: dotted host as whole remainder", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "host": "login.salesforce.com" + } + }, + { + "category": "third-party", + "metricId": "callsP95", + "seriesId": "bdpx.api.example.com", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "third-party callsP95: dotted host", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "host": "api.example.com" + } + }, + { + "category": "third-party", + "metricId": "remoteExceptions", + "seriesId": "bdpx.xitgmcd3.api.commercecloud.salesforce.com.socketReadTimeout", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "third-party remoteExceptions: host + exceptionType (last dot split)", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "host": "xitgmcd3.api.commercecloud.salesforce.com", + "exceptionType": "socketReadTimeout" + } + }, + { + "category": "third-party", + "metricId": "remoteExceptions", + "seriesId": "bdpx.login.salesforce.com.connectionTimeout", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "third-party remoteExceptions: another exception type", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "host": "login.salesforce.com", + "exceptionType": "connectionTimeout" + } + }, + { + "category": "third-party", + "metricId": "remoteExceptions", + "seriesId": "bdpx.host", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "third-party remoteExceptions: no dot (host only)", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "host": "host" + } + }, + { + "category": "ocapi", + "metricId": "totalCalls", + "seriesId": "bdpx.shop", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "OCAPI totalCalls: ocapiCategory", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "ocapiCategory": "shop" + } + }, + { + "category": "ocapi", + "metricId": "callsMean", + "seriesId": "bdpx.data", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "OCAPI callsMean: ocapiCategory", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "ocapiCategory": "data" + } + }, + { + "category": "controller", + "metricId": "callsMean", + "seriesId": "bdpx.Checkout-Begin", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "Controller wildcard: controller name", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "controller": "Checkout-Begin" + } + }, + { + "category": "controller", + "metricId": "totalCalls", + "seriesId": "bdpx.Home-Show", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "Controller wildcard: applies to any controller metric", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "controller": "Home-Show" + } + }, + { + "category": "scapi", + "metricId": "totalCalls", + "seriesId": "bdpx.shopper.auth.v1", + "context": { + "tenantId": "bdpx_prd", + "apiFamily": "shopper" + }, + "description": "Applied apiFamily filter overrides drill-down id", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "apiFamily": "shopper", + "apiVersion": "v1", + "apiName": "auth" + } + }, + { + "category": "scapi", + "metricId": "totalCalls", + "seriesId": "totalCalls", + "context": { + "tenantId": "bdpx_prd", + "apiFamily": "products", + "apiName": "shopper-products" + }, + "description": "Applied apiFamily + apiName filters", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "aggregation": "total", + "apiFamily": "products", + "apiName": "shopper-products" + } + }, + { + "category": "ocapi", + "metricId": "totalCalls", + "seriesId": "totalCalls", + "context": { + "tenantId": "bdpx_prd", + "ocapiCategory": "shop" + }, + "description": "Applied ocapiCategory filter", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "aggregation": "total", + "ocapiCategory": "shop" + } + }, + { + "category": "third-party", + "metricId": "callsCount", + "seriesId": "bdpx.login.salesforce.com", + "context": { + "tenantId": "bdpx_prd", + "thirdPartyServiceId": "my.svc" + }, + "description": "Applied thirdPartyServiceId filter (heuristic host still present)", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "host": "login.salesforce.com", + "thirdPartyServiceId": "my.svc" + } + }, + { + "category": "overall", + "metricId": "requests", + "seriesId": "bdpx Requests", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "Unrecognized pattern: prose label under series tag", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "series": "Requests" + } + }, + { + "category": "mrt", + "metricId": "errorRate", + "seriesId": "errorRate", + "context": { + "tenantId": "bdpx_prd" + }, + "description": "Fallback series echoes metric id (not captured as series tag)", + "expectedTags": { + "realm": "bdpx", + "environment": "prd", + "aggregation": "total" + } + } + ] +} diff --git a/packages/b2c-tooling-sdk/src/operations/metrics/tags.ts b/packages/b2c-tooling-sdk/src/operations/metrics/tags.ts index d8d1364a8..d70dad785 100644 --- a/packages/b2c-tooling-sdk/src/operations/metrics/tags.ts +++ b/packages/b2c-tooling-sdk/src/operations/metrics/tags.ts @@ -137,84 +137,144 @@ function stripRealmPrefix(seriesId: string, realm: string): string { type SeriesTagExtractor = (remainder: string, rawId: string, realm: string) => MetricSeriesTags; /** - * Extractor for series whose remainder is a bare API family (`product`, - * `custom`, …) — but which may instead be an HTTP status class (`2xx`) or the - * metric's own fallback id. SCAPI mixes these within a single metric. + * Strategy names for the declarative extractor catalog. Each strategy maps to a + * specific parsing rule. Some strategies require a `key` parameter. */ -const scapiFamilyOrStatus: SeriesTagExtractor = (remainder): MetricSeriesTags => { - if (/^[1-5]xx$/.test(remainder)) return {statusClass: remainder}; - return {apiFamily: remainder}; -}; +export type ExtractorStrategy = + | 'familyOrStatus' // SCAPI: remainder is apiFamily or HTTP status class (2xx) + | 'familyOrOverallAgg' // SCAPI requestLatency: overall→aggregation, else apiFamily + | 'lastSpaceSplit' // SCAPI cacheHitRate: split on last space → apiFamily + cacheStatus + | 'lastDotSplit' // third-party remoteExceptions: last dot → host + exceptionType + | 'wholeAs' // assign remainder to a single key (requires `key` field) + | 'ecdnSuccessError'; // eCDN successAndError: status class before realm, host after /** - * Per-category, per-metric extraction rules. Keyed by `category` then - * `metricId`; a category-level `*` entry applies to any metric not explicitly - * listed. Rules operate on the realm-stripped remainder (see - * {@link stripRealmPrefix}); returning `{}` means "no extra dimensions, just the - * context tags." + * A declarative rule that maps a category/metricId pair to an extraction strategy. + * Exported for build-time catalog generation (consumed by the Go SDK). */ -const EXTRACTORS: Partial>> = { - scapi: { - // `bdpx.product` → apiFamily=product; `bdpx 2xx`/`bdpx 3xx` → statusClass - totalCalls: scapiFamilyOrStatus, - requestLatency: (remainder): MetricSeriesTags => { - // `Average overall latency` is a rollup, not a per-family series. - if (/overall/i.test(remainder)) return {aggregation: 'overall'}; - return {apiFamily: remainder}; - }, - responseCount: scapiFamilyOrStatus, - errors4xx: (remainder): MetricSeriesTags => ({apiFamily: remainder}), - // `bdpx.product HIT` / `bdpx.custom MISS` → apiFamily + cacheStatus - cacheHitRate: (remainder): MetricSeriesTags => { - const spaceIdx = remainder.lastIndexOf(' '); - if (spaceIdx > 0) { - return {apiFamily: remainder.slice(0, spaceIdx), cacheStatus: remainder.slice(spaceIdx + 1)}; - } - return {apiFamily: remainder}; - }, +export interface ExtractorRule { + /** The metric category this rule applies to. */ + category: MetricCategory; + /** The metric id, or '*' to apply to all metrics in the category. */ + metricId: string; + /** The strategy to apply for extracting tags. */ + strategy: ExtractorStrategy; + /** The tag key to assign when using the 'wholeAs' strategy. */ + key?: string; +} + +/** + * The declarative catalog of all extraction rules. This is the single source of + * truth for both TypeScript and Go (exported to JSON at build time). + */ +export const EXTRACTOR_CATALOG: ExtractorRule[] = [ + // SCAPI + {category: 'scapi', metricId: 'totalCalls', strategy: 'familyOrStatus'}, + {category: 'scapi', metricId: 'requestLatency', strategy: 'familyOrOverallAgg'}, + {category: 'scapi', metricId: 'responseCount', strategy: 'familyOrStatus'}, + {category: 'scapi', metricId: 'errors4xx', strategy: 'wholeAs', key: 'apiFamily'}, + {category: 'scapi', metricId: 'cacheHitRate', strategy: 'lastSpaceSplit'}, + // OCAPI + {category: 'ocapi', metricId: 'totalCalls', strategy: 'wholeAs', key: 'ocapiCategory'}, + {category: 'ocapi', metricId: 'callsMean', strategy: 'wholeAs', key: 'ocapiCategory'}, + // Controller + {category: 'controller', metricId: '*', strategy: 'wholeAs', key: 'controller'}, + // Third-party + {category: 'third-party', metricId: 'callsCount', strategy: 'wholeAs', key: 'host'}, + {category: 'third-party', metricId: 'callsP95', strategy: 'wholeAs', key: 'host'}, + {category: 'third-party', metricId: 'remoteExceptions', strategy: 'lastDotSplit'}, + // eCDN + {category: 'ecdn', metricId: 'successAndError', strategy: 'ecdnSuccessError'}, + {category: 'ecdn', metricId: '*', strategy: 'wholeAs', key: 'host'}, +]; + +/** + * Strategy implementations. Maps strategy name to the extraction function. + */ +/** + * Parses a SCAPI drill-down remainder into its dimensions. When a request filters + * by `apiFamily`, the server returns finer-grained series ids of the form + * `{apiFamily}.{apiName}[.{apiName…}].{version}` — e.g. `shopper.auth.v1` or + * `search.shopper-search.v1`. This splits off the leading family, a trailing `vN` + * version (if present), and treats everything in between as the api name, so that + * otherwise-identical drilled-down series get distinct, groupable tags. The + * `apiFamily` is authoritative-overridden later by any applied filter. + */ +function scapiDrilldown(remainder: string): MetricSeriesTags { + const segments = remainder.split('.'); + const tags: MetricSeriesTags = {apiFamily: segments[0]}; + let rest = segments.slice(1); + const last = rest[rest.length - 1]; + if (last && /^v\d+$/.test(last)) { + tags.apiVersion = last; + rest = rest.slice(0, -1); + } + if (rest.length > 0) tags.apiName = rest.join('.'); + return tags; +} + +const STRATEGY_IMPL: Record< + ExtractorStrategy, + (remainder: string, rawId: string, realm: string, key?: string) => MetricSeriesTags +> = { + familyOrStatus: (remainder): MetricSeriesTags => { + if (/^[1-5]xx$/.test(remainder)) return {statusClass: remainder}; + // A drill-down id (`shopper.auth.v1`) carries an api name/version; a bare + // family (`product`) does not. + if (remainder.includes('.')) return scapiDrilldown(remainder); + return {apiFamily: remainder}; }, - ocapi: { - // `bdpx.shop` → ocapiCategory=shop - totalCalls: (remainder): MetricSeriesTags => ({ocapiCategory: remainder}), - callsMean: (remainder): MetricSeriesTags => ({ocapiCategory: remainder}), + familyOrOverallAgg: (remainder): MetricSeriesTags => { + // `Average overall latency` is a rollup, not a per-family series. + if (/overall/i.test(remainder)) return {aggregation: 'overall'}; + if (remainder.includes('.')) return scapiDrilldown(remainder); + return {apiFamily: remainder}; }, - controller: { - // `bdpx.Home-Show` → controller=Home-Show (applies to every controller metric) - '*': (remainder): MetricSeriesTags => ({controller: remainder}), + lastSpaceSplit: (remainder): MetricSeriesTags => { + // `bdpx.product HIT` / `bdpx.custom MISS` → apiFamily + cacheStatus + const spaceIdx = remainder.lastIndexOf(' '); + if (spaceIdx > 0) { + return {apiFamily: remainder.slice(0, spaceIdx), cacheStatus: remainder.slice(spaceIdx + 1)}; + } + return {apiFamily: remainder}; }, - 'third-party': { - // `bdpx.login.salesforce.com` → host; the host itself contains dots, so we - // treat the whole remainder as the host for call/latency metrics. - callsCount: (remainder): MetricSeriesTags => ({host: remainder}), - callsP95: (remainder): MetricSeriesTags => ({host: remainder}), - // `bdpx.host.socketReadTimeout` → host + exceptionType. The exception type is - // the final dot-segment; everything before it is the (dotted) host. This is - // only unambiguous because we key on the remoteExceptions metric. - remoteExceptions: (remainder): MetricSeriesTags => { - const lastDot = remainder.lastIndexOf('.'); - if (lastDot > 0) { - return {host: remainder.slice(0, lastDot), exceptionType: remainder.slice(lastDot + 1)}; - } - return {host: remainder}; - }, + lastDotSplit: (remainder): MetricSeriesTags => { + // `bdpx.host.socketReadTimeout` → host + exceptionType + const lastDot = remainder.lastIndexOf('.'); + if (lastDot > 0) { + return {host: remainder.slice(0, lastDot), exceptionType: remainder.slice(lastDot + 1)}; + } + return {host: remainder}; }, - ecdn: { - // `2xx bdpx.host` (status class BEFORE the realm) → statusClass + host; other - // eCDN metrics are just `bdpx.host` → host. Operates on the raw id because - // the realm is not a leading prefix here. - successAndError: (_remainder, rawId, realm): MetricSeriesTags => { - const spaceIdx = rawId.indexOf(' '); - if (spaceIdx > 0) { - const statusClass = rawId.slice(0, spaceIdx); - const host = stripRealmPrefix(rawId.slice(spaceIdx + 1), realm); - return {statusClass, host}; - } - return {host: stripRealmPrefix(rawId, realm)}; - }, - '*': (remainder) => ({host: remainder}), + wholeAs: (remainder, _rawId, _realm, key): MetricSeriesTags => { + if (!key) throw new Error('wholeAs strategy requires a key parameter'); + return {[key]: remainder}; + }, + ecdnSuccessError: (_remainder, rawId, realm): MetricSeriesTags => { + // `2xx bdpx.host` (status class BEFORE the realm) → statusClass + host + const spaceIdx = rawId.indexOf(' '); + if (spaceIdx > 0) { + const statusClass = rawId.slice(0, spaceIdx); + const host = stripRealmPrefix(rawId.slice(spaceIdx + 1), realm); + return {statusClass, host}; + } + return {host: stripRealmPrefix(rawId, realm)}; }, }; +/** + * Pre-built extractor index for fast lookup. Built from the declarative catalog. + */ +const EXTRACTORS: Partial>> = (() => { + const index: Partial>> = {}; + for (const rule of EXTRACTOR_CATALOG) { + if (!index[rule.category]) index[rule.category] = {}; + const impl = STRATEGY_IMPL[rule.strategy]; + index[rule.category]![rule.metricId] = (remainder, rawId, realm) => impl(remainder, rawId, realm, rule.key); + } + return index; +})(); + /** * Extracts the dimension tags for a single series id. * @@ -260,12 +320,17 @@ export function parseSeriesTags(params: { const extractor = categoryRules?.[metricId] ?? categoryRules?.['*']; const remainder = stripRealmPrefix(seriesId, realm); - if (extractor) { + if (remainder === metricId) { + // The series id is just the metric id echoed back (e.g. `cacheHitRate`, + // `errors4xx`) — a rollup/aggregate series carrying no per-series dimension. + // Don't run the extractor (which would mis-tag it as apiFamily/host/etc.) and + // don't record a `series` tag; identity tags alone are correct here. + tags.aggregation = 'total'; + } else if (extractor) { Object.assign(tags, extractor(remainder, seriesId, realm)); - } else if (remainder && remainder !== metricId) { + } else if (remainder) { // No rule for this category/metric. Preserve the (realm-stripped) remainder - // so nothing is lost, unless it is just the metric id echoed back (a - // value-less fallback series). + // so nothing is lost. tags.series = remainder; } diff --git a/packages/b2c-tooling-sdk/test/operations/metrics/tags-golden.test.ts b/packages/b2c-tooling-sdk/test/operations/metrics/tags-golden.test.ts new file mode 100644 index 000000000..ce09de102 --- /dev/null +++ b/packages/b2c-tooling-sdk/test/operations/metrics/tags-golden.test.ts @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {expect} from 'chai'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import {fileURLToPath} from 'node:url'; +import type {MetricCategory} from '../../../src/operations/metrics/index.js'; +import {parseSeriesTags, type MetricsTagContext, type MetricSeriesTags} from '../../../src/operations/metrics/tags.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const GOLDEN_PATH = path.resolve(__dirname, '../../../specs/metrics-tags.golden.json'); + +interface GoldenTestCase { + category: MetricCategory; + metricId: string; + seriesId: string; + context: MetricsTagContext; + expectedTags: MetricSeriesTags; + description?: string; +} + +interface GoldenFixture { + version: string; + generatedAt: string; + description: string; + testCases: GoldenTestCase[]; +} + +describe('parseSeriesTags (golden fixture)', () => { + let fixture: GoldenFixture; + + before(() => { + const raw = fs.readFileSync(GOLDEN_PATH, 'utf-8'); + fixture = JSON.parse(raw); + }); + + it('should have a valid golden fixture', () => { + expect(fixture.version).to.equal('1.0.0'); + expect(fixture.testCases).to.be.an('array').with.length.greaterThan(0); + }); + + for (const tc of JSON.parse(fs.readFileSync(GOLDEN_PATH, 'utf-8')).testCases as GoldenTestCase[]) { + it(tc.description ?? `${tc.category}:${tc.metricId} "${tc.seriesId}"`, () => { + const actual = parseSeriesTags({ + category: tc.category, + metricId: tc.metricId, + seriesId: tc.seriesId, + context: tc.context, + }); + expect(actual).to.deep.equal(tc.expectedTags); + }); + } +}); diff --git a/packages/b2c-tooling-sdk/test/operations/metrics/tags.test.ts b/packages/b2c-tooling-sdk/test/operations/metrics/tags.test.ts index f5aac72e6..0ca5493de 100644 --- a/packages/b2c-tooling-sdk/test/operations/metrics/tags.test.ts +++ b/packages/b2c-tooling-sdk/test/operations/metrics/tags.test.ts @@ -152,10 +152,15 @@ describe('parseSeriesTags', () => { expect(tags.realm).to.equal('bdpx'); }); - it('does not echo the metric id back as a `series` tag (value-less fallback series)', () => { + it('tags a metric-id-echo series as an aggregate rollup, not a `series`/apiFamily dimension', () => { + // A series whose id is just the metric id (e.g. `errorRate`, `cacheHitRate`, + // `errors4xx`) is a rollup carrying no per-series dimension. It must not be + // mis-tagged as apiFamily/series; it is marked aggregation=total instead. const tags = parseSeriesTags({category: 'mrt', metricId: 'errorRate', seriesId: 'errorRate', context: CTX}); expect(tags.series).to.equal(undefined); - expect(Object.keys(tags)).to.deep.equal(['realm', 'environment']); + expect(tags.apiFamily).to.equal(undefined); + expect(tags.aggregation).to.equal('total'); + expect(Object.keys(tags).sort()).to.deep.equal(['aggregation', 'environment', 'realm']); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f441d773..24014bbd0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -181,10 +181,10 @@ importers: version: 1.1.2(typedoc-plugin-markdown@4.9.0(typedoc@0.28.14(typescript@5.9.3))) vitepress: specifier: 2.0.0-alpha.17 - version: 2.0.0-alpha.17(@types/node@22.19.0)(change-case@5.4.4)(fuse.js@7.1.0)(postcss@8.5.15)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.9.0) + version: 2.0.0-alpha.17(@types/node@22.19.0)(change-case@5.4.4)(fuse.js@7.1.0)(postcss@8.5.15)(terser@5.49.0)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.9.0) vitepress-plugin-group-icons: specifier: ^1.7.5 - version: 1.7.5(vite@7.3.5(@types/node@22.19.0)(tsx@4.20.6)(yaml@2.9.0)) + version: 1.7.5(vite@7.3.5(@types/node@22.19.0)(terser@5.49.0)(tsx@4.20.6)(yaml@2.9.0)) packages/b2c-cli: dependencies: @@ -336,7 +336,7 @@ importers: version: 9.39.1 '@modelcontextprotocol/inspector': specifier: ^0.18.0 - version: 0.18.0(@types/node@22.19.0)(@types/react-dom@18.3.1)(@types/react@18.3.12)(typescript@5.9.3) + version: 0.18.0(@swc/core@1.15.43(@swc/helpers@0.5.23))(@types/node@22.19.0)(@types/react-dom@18.3.1)(@types/react@18.3.12)(typescript@5.9.3) '@oclif/prettier-config': specifier: 'catalog:' version: 0.2.1 @@ -401,6 +401,70 @@ importers: specifier: 'catalog:' version: 8.54.0(eslint@9.39.1)(typescript@5.9.3) + packages/b2c-grafana-datasource: + dependencies: + '@grafana/data': + specifier: ^10.4.0 + version: 10.4.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@grafana/runtime': + specifier: ^10.4.0 + version: 10.4.19(@react-spectrum/provider@3.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@grafana/ui': + specifier: ^10.4.0 + version: 10.4.19(@react-spectrum/provider@3.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + devDependencies: + '@grafana/eslint-config': + specifier: ^7.0.0 + version: 7.0.0 + '@grafana/tsconfig': + specifier: ^2.0.0 + version: 2.2.0 + '@swc/core': + specifier: ^1.4.0 + version: 1.15.43(@swc/helpers@0.5.23) + '@types/node': + specifier: ^20.11.0 + version: 20.19.43 + '@types/react': + specifier: ^18.2.0 + version: 18.3.12 + '@types/react-dom': + specifier: ^18.2.0 + version: 18.3.1 + '@types/webpack': + specifier: ^5.28.5 + version: 5.28.5(@swc/core@1.15.43(@swc/helpers@0.5.23))(postcss@8.5.15)(webpack-cli@5.1.4) + copy-webpack-plugin: + specifier: ^12.0.2 + version: 12.0.2(webpack@5.108.4) + css-loader: + specifier: ^6.10.0 + version: 6.11.0(webpack@5.108.4) + style-loader: + specifier: ^3.3.4 + version: 3.3.4(webpack@5.108.4) + swc-loader: + specifier: ^0.2.6 + version: 0.2.7(@swc/core@1.15.43(@swc/helpers@0.5.23))(webpack@5.108.4) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.15.43(@swc/helpers@0.5.23))(@types/node@20.19.43)(typescript@5.9.3) + typescript: + specifier: ^5.3.0 + version: 5.9.3 + webpack: + specifier: ^5.90.0 + version: 5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(postcss@8.5.15)(webpack-cli@5.1.4) + webpack-cli: + specifier: ^5.1.4 + version: 5.1.4(webpack@5.108.4) + packages/b2c-plugin-example-config: dependencies: '@salesforce/b2c-tooling-sdk': @@ -1053,6 +1117,24 @@ importers: packages: + '@adobe/react-spectrum-ui@1.2.1': + resolution: {integrity: sha512-wcrbEE2O/9WnEn6avBnaVRRx88S5PLFsPLr4wffzlbMfXeQsy+RMQwaJd3cbzrn18/j04Isit7f7Emfn0dhrJA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + + '@adobe/react-spectrum-workflow@2.3.5': + resolution: {integrity: sha512-b53VIPwPWKb/T5gzE3qs+QlGP5gVrw/LnWV3xMksDU+CRl3rzOKUwxIGiZO8ICyYh1WiyqY4myGlPU/nAynBUg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + + '@adobe/react-spectrum@3.47.2': + resolution: {integrity: sha512-QxsE7bPBGpzwYofACF0RAL/Zs3p0u9HNvCDf9LEN87rEGFpkagIB+T+TSZ0xbl6BJ6z7S02m3zAFk3s9FoHJpQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} @@ -1560,31 +1642,75 @@ packages: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.28.5': resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + '@babel/parser@7.29.2': resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/runtime@7.28.4': resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@braintree/sanitize-url@7.0.0': + resolution: {integrity: sha512-GMu2OJiTd1HSe74bbJYQnVvELANpYiGFZELyyTM1CR0sdv5ReQAcJ/c/8pIrPab3lO11+D+EpuGLUxqz+y832g==} + '@changesets/apply-release-plan@7.1.1': resolution: {integrity: sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==} @@ -1661,6 +1787,10 @@ packages: '@dabh/diagnostics@2.0.8': resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} + '@discoveryjs/json-ext@0.5.7': + resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} + engines: {node: '>=10.0.0'} + '@docsearch/css@4.6.2': resolution: {integrity: sha512-fH/cn8BjEEdM2nJdjNMHIvOVYupG6AIDtFVDgIZrNzdCSj4KXr9kd+hsehqsNGYjpUjObeKYKvgy/IwCb1jZYQ==} @@ -1679,6 +1809,57 @@ packages: '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + + '@emotion/css@11.11.2': + resolution: {integrity: sha512-VJxe1ucoMYMS7DkiMdC2T7PWNbrEI0a39YRiyDvK2qq4lXwjRbVP/z4lpG+odCsRzadlR+1ywwrTzhdm5HNdew==} + + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/react@11.11.3': + resolution: {integrity: sha512-Cnn0kuq4DoONOMcnoVsTOR8E+AdnKFf//6kUWc4LCdnxj31pZWn7rIULd6Y7/Js1PiPHzn7SKCM9vB/jBni8eA==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0': + resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} + peerDependencies: + react: '>=16.8.0' + + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + + '@emotion/weak-memoize@0.3.1': + resolution: {integrity: sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==} + + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + + '@es-joy/jsdoccomment@0.40.1': + resolution: {integrity: sha512-YORCdZSusAlBrFpZ77pJjc5r1bQs5caPWtAu+WWmiSo+8XaUzseapVrfAtiRFbQWnrBxxLLEwF6f6ZG/UgCQCg==} + engines: {node: '>=16'} + '@es-joy/jsdoccomment@0.50.2': resolution: {integrity: sha512-YAdE/IJSpwbOTiaURNCKECdAwqrJuFiZhylmesBcIRawtYKnBR2wxPhoIewMg+Yu+QuYvHfJNReWpoxGBKOChA==} engines: {node: '>=18'} @@ -2192,10 +2373,18 @@ packages: resolution: {integrity: sha512-pHoYRWS08oeU0qVez1pZCcbqHzoJnM5VMtrxH2nWDJ0ukq9DkwWV1BTY+PWK+eWBbndN9W0O9WjJTyAHsDoPOg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/eslintrc@3.3.1': resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@8.52.0': + resolution: {integrity: sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/js@9.39.1': resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2228,12 +2417,72 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' + '@floating-ui/react@0.26.9': + resolution: {integrity: sha512-p86wynZJVEkEq2BBjY/8p2g3biQ6TlgT4o/3KgFKyTWoJLU1GZ8wpctwRqtkEl2tseYA+kw7dBAIDFcednfI5w==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@formatjs/ecma402-abstract@2.3.6': + resolution: {integrity: sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==} + + '@formatjs/fast-memoize@2.2.7': + resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==} + + '@formatjs/icu-messageformat-parser@2.11.4': + resolution: {integrity: sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==} + + '@formatjs/icu-skeleton-parser@1.8.16': + resolution: {integrity: sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==} + + '@formatjs/intl-localematcher@0.6.2': + resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==} + '@gerrit0/mini-shiki@3.15.0': resolution: {integrity: sha512-L5IHdZIDa4bG4yJaOzfasOH/o22MCesY0mx+n6VATbaiCtMeR59pdRqYk4bEiQkIHfxsHPNgdi7VJlZb2FhdMQ==} + '@grafana/data@10.4.19': + resolution: {integrity: sha512-Lz5I5PhOQb6hhHpAkwo619+RmHQRM8K1oj+8DdfPbjaEUqj3v0UvgTCmjFOkIJ5JYD/ccNqrmMvF2LR6sA7yFQ==} + peerDependencies: + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + + '@grafana/e2e-selectors@10.4.19': + resolution: {integrity: sha512-LZs6mDFCxxAn7UJVPgRyh29vlMyF728EMe/yePTv7kcIxAgQwrZs30XwVoIsGv1jOmbgfdnPHaB8LxgC9EBNcQ==} + + '@grafana/eslint-config@7.0.0': + resolution: {integrity: sha512-LSN6RYntCx9Z7qo5Wm9tjtBfK1vPzvMxQWHuhS0qh9MSMrlC8bZ7FPHFgg9N65q7TYA2SaH2Onz3BLInZFYtDw==} + + '@grafana/faro-core@1.19.0': + resolution: {integrity: sha512-Juo5G/aviSh3XqSGGr6D61noAC8sb+oCawBsv545ILEeOQdINyzRaoQdRpnXEY3DLS9LYtL0PYhvHZiP3rlscQ==} + + '@grafana/faro-web-sdk@1.19.0': + resolution: {integrity: sha512-3u74mV2uBWqoF6WBx71p0vtkaS1Z0QbGoZ8tuX5yiYnIybqnhKdGkApFUi7q5se0tMPIeJdMVoRFdLU8f9hfAw==} + + '@grafana/runtime@10.4.19': + resolution: {integrity: sha512-zQ9RRiCLQ0ED5EyseTKsxAIJ12fPDmpw8aqlyjbTioQJmGcMgABRN2zqPVbKXwSORE8AzCAVVq3BWYKmBDaZ6Q==} + peerDependencies: + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + + '@grafana/schema@10.4.19': + resolution: {integrity: sha512-4kH+Ryiy529fBDZe97qnGrtpmESYaNX8/ef8WsvsUbP1ZheopUkd18r/+VTc18tX7ujOpuThXCdOBDsptj3qJw==} + + '@grafana/tsconfig@1.2.0-rc1': + resolution: {integrity: sha512-+SgQeBQ1pT6D/E3/dEdADqTrlgdIGuexUZ8EU+8KxQFKUeFeU7/3z/ayI2q/wpJ/Kr6WxBBNlrST6aOKia19Ag==} + + '@grafana/tsconfig@2.2.0': + resolution: {integrity: sha512-hhhXetqhSKTfgErStfWVPhqo4JPi+lNqBJNhBF6RT4ZIM4vwKxEfnfsyswT+NwHIXNrOz23xoOGeKWPpxmaHWQ==} + + '@grafana/ui@10.4.19': + resolution: {integrity: sha512-zgi1kudKMH6eG/1uKa1FeVWhf9RUnzHY3W2mYS94nGPP1ZNe1e9q0ABK9fQq68bWFMcwk7nFSSCjDve7dpKrVw==} + peerDependencies: + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + '@h4ad/serverless-adapter@4.4.0': resolution: {integrity: sha512-Cj/dBqhOmmzf1ILrXPppEA4e8qWGSm/Mod0uAsftupmMrCGUjvzLq4PBvevnK9ASC5WPbAtnbgkwao+f4tDklw==} engines: {node: '>=18.0.0'} @@ -2313,6 +2562,11 @@ packages: resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} engines: {node: '>=18.18.0'} + '@humanwhocodes/config-array@0.11.14': + resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} @@ -2321,6 +2575,10 @@ packages: resolution: {integrity: sha512-KWiFQpSAqEIyrTXko3hFNLeQvSK8zXlJQzhhxsyVn58WFRYXST99b3Nqnu+ttOtjds2Pl2grUHGpe2NzhPynuQ==} engines: {node: '>=18'} + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + '@humanwhocodes/retry@0.4.3': resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} @@ -2632,6 +2890,18 @@ packages: '@types/node': optional: true + '@internationalized/date@3.12.2': + resolution: {integrity: sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==} + + '@internationalized/message@3.1.10': + resolution: {integrity: sha512-nc0Or6EdWHqZRcsXb6P9hBIpLsfSl/ILh0rk5h/OVBpzmhdExXtPy2cQtWsq8XKRBpRHwDNnAHt4OpolcB7dog==} + + '@internationalized/number@3.6.7': + resolution: {integrity: sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg==} + + '@internationalized/string@3.2.9': + resolution: {integrity: sha512-kzP/M/mbQxODlmOt4bIQZ2SBVUWUSqMLXooXixnX7noche8WHaQcA+nwFN1K2KCF/cp+LDUhcJsCicwkvhD1pg==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -2644,10 +2914,16 @@ packages: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -2657,12 +2933,29 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@leeoniya/ufuzzy@1.0.14': + resolution: {integrity: sha512-/xF4baYuCQMo+L/fMSUrZnibcu0BquEGnbxfVPiZhs/NbJeKj4c/UmFpQzW9Us0w45ui/yYW3vyaqawhNYsTzA==} + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + '@mapbox/jsonlint-lines-primitives@2.0.3': + resolution: {integrity: sha512-0SElaV0uMxEnxzBhhX9WTuPyUeMsAN/SS0i16tjuba4/mio63MG9khjC1a0JAiPGXAwvwm4UfHJURCN7nyudQg==} + engines: {node: '>= 22'} + + '@mapbox/mapbox-gl-style-spec@13.28.0': + resolution: {integrity: sha512-B8xM7Fp1nh5kejfIl4SWeY0gtIeewbuRencqO3cJDrCHZpaPg7uY+V8abuR+esMeuOjRl5cLhVTP40v+1ywxbg==} + hasBin: true + + '@mapbox/point-geometry@0.1.0': + resolution: {integrity: sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==} + + '@mapbox/unitbezier@0.0.0': + resolution: {integrity: sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==} + '@modelcontextprotocol/inspector-cli@0.18.0': resolution: {integrity: sha512-QMPjKx8zKmX17S1LF2gWuwbYglKexkdgB0HhKZFXzGrQ0MYoKUsIgokMyV48xr4LipaLS3b2v3ut3nV/jhWeSg==} hasBin: true @@ -2690,6 +2983,16 @@ packages: '@cfworker/json-schema': optional: true + '@monaco-editor/loader@1.7.0': + resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==} + + '@monaco-editor/react@4.6.0': + resolution: {integrity: sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==} + peerDependencies: + monaco-editor: '>= 0.25.0 < 1' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + '@mswjs/interceptors@0.40.0': resolution: {integrity: sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==} engines: {node: '>=18'} @@ -2766,9 +3069,60 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@opentelemetry/api-logs@0.202.0': + resolution: {integrity: sha512-fTBjMqKCfotFWfLzaKyhjLvyEyq5vDKTTFfBmx21btv3gvy8Lq6N5Dh2OzqeuN4DjtpSvNT1uNVfg08eD2Rfxw==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/core@2.9.0': + resolution: {integrity: sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/otlp-transformer@0.202.0': + resolution: {integrity: sha512-5XO77QFzs9WkexvJQL9ksxL8oVFb/dfi9NWQSq7Sv0Efr9x3N+nb1iklP1TeVgxqJ7m1xWiC/Uv3wupiQGevMw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/resources@2.0.1': + resolution: {integrity: sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.202.0': + resolution: {integrity: sha512-pv8QiQLQzk4X909YKm0lnW4hpuQg4zHwJ4XBd5bZiXcd9urvrJNoNVKnxGHPiDVX/GiLFvr5DMYsDBQbZCypRQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.0.1': + resolution: {integrity: sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.0.1': + resolution: {integrity: sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.43.0': + resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} + engines: {node: '>=14'} + '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + '@petamoriken/float16@3.9.3': + resolution: {integrity: sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==} + '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -2792,6 +3146,9 @@ packages: resolution: {integrity: sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==} engines: {node: '>=12'} + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -3228,6 +3585,147 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@rc-component/portal@1.1.2': + resolution: {integrity: sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/trigger@1.18.3': + resolution: {integrity: sha512-Ksr25pXreYe1gX6ayZ1jLrOrl9OAUHUqnuhEx6MeHnNa1zVM5Y2Aj3Q35UrER0ns8D2cJYtmJtVli+i+4eKrvA==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@react-aria/button@3.15.1': + resolution: {integrity: sha512-SBMn8ZLvjuWCpSqi6o1hOjsqQqkdYFfzIdl/0LgNPUpTclkJuMx7gNXfM3mjgxzSCoS5CD/XdicvqJanMw6jCw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/dialog@3.5.11': + resolution: {integrity: sha512-oT+FBOtPZRWVBxPt1K8F5XaKGYpi+ZV3oFFzub8w+D6m+9WN4pktUx7YBz95Kunw7M1HcAsyQZX0fsAuDPL7Rw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + + '@react-aria/dialog@3.6.1': + resolution: {integrity: sha512-Eo3Kj23TjENuERUYrGkH+VN1PJvK8/zep76pfwBg29erUVDpGdfagYRq82aGcxJ/ohG8iRxZDwp2tTrU1RG1MQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/focus@3.16.1': + resolution: {integrity: sha512-3ZEYc+hWqDQX7fA54ZOTkED8OGXs9+K9fYmjD1IdjZJAJS/2/AJ95PgIQ29zBkl9D9TAi4Nb3tJ/3+H/02UzoA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + + '@react-aria/i18n@3.13.1': + resolution: {integrity: sha512-z56ZYcbfpNmMyiGLhyEjytpmEfoTlBaksk84q4kds3HvNkf7QWKj+DJVfVDrJX+c1LyuBsszLSX7yxJRiHsYKQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/interactions@3.28.1': + resolution: {integrity: sha512-Bqb+HrD5I5MHS2SKBhISYqo2SW8Y2dfzgF/Y1lIJq7xqLxheo9vzxPGEHhz+XzkgGfoqEJx8A6a3C7uiqS3HWA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/overlays@3.21.0': + resolution: {integrity: sha512-ulE5RQP3ZUFqY6Zok4L/CCZW5HCPZeuyDEezPw4/4Y/WD6TjGZ1ChbPuGsAl+X+fo/iKTpe7joN4kYrKmTb5WA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + + '@react-aria/overlays@3.32.1': + resolution: {integrity: sha512-jjVLcEK5qaGsz3SmW+eLV3QFiJzdDFzgNofPwvzBS1KTPox0a2x5u1ITUPmHwyUNiyexy531h5eVjN3tULEzHQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/ssr@3.10.1': + resolution: {integrity: sha512-jn038/ZYmu6DpfXJ6r2U9zFFppjbc9wnApPJSCxao2RZVEqep4YyoniHSy8qv6V21/xyS4IV7W9a+X2jOjSuag==} + engines: {node: '>= 12'} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/utils@3.23.1': + resolution: {integrity: sha512-iXibf9ojqdoygbvy/++v5cKLKgjc/5ZmKV8/9u/2Hkpha1cf5Td/Z+Vl42B6giUBAsuDio5kuZYfYC7Uk+t8ag==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + + '@react-aria/visually-hidden@3.9.1': + resolution: {integrity: sha512-PWuth+NTmUiBJAIyrfk7dJ5BxOBupDt0iFGlBiYr5FElSYvwN9LAk1kgkzm6hT2qzq4FmYdQgBSPkGeaxOui6w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-spectrum/button@3.18.1': + resolution: {integrity: sha512-FW+1d6zfKesMLaBrsRsnnYnsLfvf2qFKuatkCo62o1oGUBtEYI/kae+lwGwlfiX5q9P5OgR2y1IqKtxabf/JKQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-spectrum/dialog@3.10.1': + resolution: {integrity: sha512-RMoZkKswUMFiEnAHZljEV1ybpMUjrS5RxkwWb3IFMo+ZtnLIeIT87k4DxoUJKOVrw3bnSsLRSTw1Fvf7jZ/eFw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-spectrum/overlays@5.10.1': + resolution: {integrity: sha512-VSpplRbzYA/Hmg+fR7fonj/Q3jq7mJHD6A5f189teX5/uRiTk7P4DogBYfU1a70BAq3xBEA4hV3QpA8i9pK8jQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-spectrum/provider@3.11.1': + resolution: {integrity: sha512-TsoNdVdmlQ7L+75ILq5Yb3+wp/I1AtIeat0o+Y+ZBxP+TtWpwT1ZtCB5l3cplFVzHzOpZlzO0VaDrDP9ElGYDw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-stately/overlays@3.7.1': + resolution: {integrity: sha512-vGw9f8i5kPvaQqvvQ8iIhPhJZorwtg2rXycqnUNXkNLadewh1S0ocbnRvrb4HW/GGC37rFmGcG1fYCHA/WIn6w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-stately/utils@3.12.1': + resolution: {integrity: sha512-NqKfzrknpfwiewx7R2vk1P+CneClInPDsIhw15+jOcUYSEfej0nta4cJywuKQJ2gsPwqX/ojDNixedCve9FWGw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-types/button@3.16.0': + resolution: {integrity: sha512-Z5///n2Y1jtF0gokBq2Y1K1cpOwsWZ24HPeAm3eEmZrbBXMrxC2oEA5ZThsSHuIGsqiyNJiQ2scsDftmr+PkZw==} + peerDependencies: + '@react-spectrum/provider': ^3.0.0 + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-types/dialog@3.6.0': + resolution: {integrity: sha512-vvxohmsTRZWE/saaJt6mMy3ONA4xbQTSk1okfMUK6OMSp/VpLBRLCz/2/myiMK3UIBCagUnrwzOwbk9whnFx0g==} + peerDependencies: + '@react-spectrum/provider': ^3.0.0 + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-types/overlays@3.10.0': + resolution: {integrity: sha512-cgrcOTxy6ac0kiphQOkc8mj5artZMB/XVrFgukRZ2FcbYNEERpg2VQ5ztd0+H1ER7O0kx7AmwHxdut+x1EAjrw==} + peerDependencies: + '@react-spectrum/provider': ^3.0.0 + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-types/shared@3.36.0': + resolution: {integrity: sha512-DkP/H0C2YjjS7gZWKNqOmU8a16qHPjQNdzMwmTq9SzplM6Iw0kVMTZ0OIoe6FOgGqa+FwMsE2QbPjh/n3g/jXQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + '@redocly/ajv@8.17.1': resolution: {integrity: sha512-EDtsGZS964mf9zAUXAl9Ew16eYbeyAFWhsPr0fX6oaJxgd8rApYlPBf0joyhnUHz88WxrigyFtTaqqzXNzPgqw==} @@ -3913,6 +4411,20 @@ packages: '@so-ric/colorspace@1.1.6': resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} + '@spectrum-icons/ui@3.7.1': + resolution: {integrity: sha512-veQymocUYo5OciXQajSailOdbWe+k6+2ehfF8D4d0V923D4xOUadtT253xXZ5vEQjPat6Kyp2WDKeQNjd7kL1w==} + peerDependencies: + '@adobe/react-spectrum': ^3.47.0 + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@spectrum-icons/workflow@4.3.1': + resolution: {integrity: sha512-kDF+/EbFVyLGytotqqdYt4uSij4j/PQmDQO5km/C6DyzKjyuic3FnSBFinR+mA6oFv1OjMcLvrrDBqK3wbqRlA==} + peerDependencies: + '@adobe/react-spectrum': ^3.47.0 + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + '@stylistic/eslint-plugin@3.1.0': resolution: {integrity: sha512-pA6VOrOqk0+S8toJYhQGv2MWpQQR0QpeUo9AhNkC49Y26nxBQ/nH1rta9bUU1rPw2fJ1zZEMV5oCX5AazT7J2g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3925,6 +4437,102 @@ packages: peerDependencies: eslint: '>=9.0.0' + '@swc/core-darwin-arm64@1.15.43': + resolution: {integrity: sha512-v1aVuvXdo/BHxJzco9V2xpHrvwWmhfS8t6gziY5wJxd+Z2h8AeJRnAwPD8itCDaGXVBwJ/CaKfxEzTkG0Va0OA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.15.43': + resolution: {integrity: sha512-lp3d4Lamc8dt5huYdGLSR+9hLxmfr1jb0l+4XXG2zPqZwYWRN9R0U2qYoTrggiU2RWW0oV9VbWM3kBnqIc2kdQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.15.43': + resolution: {integrity: sha512-JWTQQELtsG5GgphDrr/XqqmM2pDN3cZqbMS0Mrg+iTiXL3F74sn/S2IyYE/5u4h2KLkTf9qQ7dXyxsbx7YzkeA==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.15.43': + resolution: {integrity: sha512-B4otJRdPWIsmiSBf0uG7Z/+vMWmkufjz5MmYxubwKuZazDW14Zd3symga1N62QR4RT+kEFeHEgsXfZGyn/w0hw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-arm64-musl@1.15.43': + resolution: {integrity: sha512-6zB6OnpViBxYy4tgY3v2i6AZY9fwkcHZ032UOwtwUuW1d19sdT07qF0kZe6/3UR1tUaK6jjg2rmVcUIBCEYVjQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@swc/core-linux-ppc64-gnu@1.15.43': + resolution: {integrity: sha512-coxE1ZWdB3uSDVNoEtYNrRi/1epvckZx9cTJ8ICUxTMTxGk+yvQ/Twacp3ruZSaMPGCriUjP86C37VhaT6nyRg==} + engines: {node: '>=10'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-s390x-gnu@1.15.43': + resolution: {integrity: sha512-lXfLhs+LpBsD5inuYx+YDH5WsPPBQ95KPUiy8P5wq9ob9xKDZFqwNfU2QW6bGO8NqRO/H9JQomTSt5Yyh+FGfA==} + engines: {node: '>=10'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@swc/core-linux-x64-gnu@1.15.43': + resolution: {integrity: sha512-07XnKwTmKy8TGOZG3D9fRnLWGynxPjwQnZLVmBFbo6F+7vHYzBIOuwXEhemrChBWb6yDNZsVCcMWCPX6FDD2xg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-x64-musl@1.15.43': + resolution: {integrity: sha512-TJc+bsSIaBh+hZvZ5GRtW/K1bw66TJ9vsUwvVIsZdiWxU5ObLwZvfcnZ3UpgVfMnFibRes9uriJrQNBHEEogRQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@swc/core-win32-arm64-msvc@1.15.43': + resolution: {integrity: sha512-jfd7s2/bUQYkOHLs+LWQNKZdmDa8+sufKLllhpWAhVQ2GDCwsHe3vR/j+OSiItZNtkzFuaawa3+SAKz9y5gYfw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.15.43': + resolution: {integrity: sha512-rLAE8JvucqEW1ZGohxPQrQWPBQeJG4+ypKbWfdlU/qmKScvCkxf9/Jxnzki1dkUQCQ7P5Enp13RlvqOlvx/32g==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.15.43': + resolution: {integrity: sha512-h8MLDHZcfIukwQWj03rIJZx1I0E81AYj2X7J/nGErG4nz+QAv6G1Z+peotvinL3lqpbo32tLYSMFo32/ySzxKg==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.15.43': + resolution: {integrity: sha512-1CuKjFkPxIgGdeHVuNbkxmBxkcbdc08u0aiI43pFq6yY1tTVKmXT9hFEooyyKs/sJ3xf1GPHyEwTtk9Xl8dvQw==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} + + '@swc/types@0.1.27': + resolution: {integrity: sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==} + '@szmarczak/http-timer@5.0.1': resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} engines: {node: '>=14.16'} @@ -3983,6 +4591,12 @@ packages: '@types/cors@2.8.19': resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + '@types/ejs@3.1.5': resolution: {integrity: sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==} @@ -4004,6 +4618,11 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/hoist-non-react-statics@3.3.7': + resolution: {integrity: sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==} + peerDependencies: + '@types/react': '*' + '@types/http-cache-semantics@4.0.4': resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} @@ -4016,6 +4635,9 @@ packages: '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + '@types/js-cookie@2.2.7': + resolution: {integrity: sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==} + '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} @@ -4058,12 +4680,18 @@ packages: '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + '@types/node@20.19.43': + resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} + '@types/node@22.19.0': resolution: {integrity: sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} @@ -4076,12 +4704,23 @@ packages: '@types/react-dom@18.3.1': resolution: {integrity: sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==} + '@types/react-redux@7.1.34': + resolution: {integrity: sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==} + + '@types/react-transition-group@4.4.12': + resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} + peerDependencies: + '@types/react': '*' + '@types/react@18.3.12': resolution: {integrity: sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==} '@types/sarif@2.1.7': resolution: {integrity: sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==} + '@types/semver@7.7.1': + resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + '@types/send@1.2.1': resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} @@ -4103,6 +4742,9 @@ packages: '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@types/string-hash@1.1.3': + resolution: {integrity: sha512-p6skq756fJWiA59g2Uss+cMl6tpoDGuCBuxG0SI1t0NwJmYOU66LAMS6QiCgu7cUh3/hYCaMl5phcCW1JP5wOA==} + '@types/superagent@8.1.10': resolution: {integrity: sha512-nbt4IWXABhW0jGmmpRzCFNlbmwCTzZ2gTUsNIr+X+ItdqPms+PAJZbWsNzpS2USqXjcoNLQcO6nXo60zcPQiIg==} @@ -4118,6 +4760,9 @@ packages: '@types/triple-beam@1.3.5': resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -4127,12 +4772,26 @@ packages: '@types/web-bluetooth@0.0.21': resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + '@types/webpack@5.28.5': + resolution: {integrity: sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==} + '@types/wrap-ansi@3.0.0': resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==} '@types/xml2js@0.4.14': resolution: {integrity: sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==} + '@typescript-eslint/eslint-plugin@6.18.1': + resolution: {integrity: sha512-nISDRYnnIpk7VCFrGcu1rnZfM1Dh9LRHnfgdkjcbi/l7g16VYRri3TjXi9Ir4lOZSw5N/gnV/3H7jIPQ8Q4daA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + '@typescript-eslint/eslint-plugin@8.54.0': resolution: {integrity: sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4141,6 +4800,16 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/parser@6.18.1': + resolution: {integrity: sha512-zct/MdJnVaRRNy9e84XnVtRv9Vf91/qqe+hZJtKanjojud4wAVy/7lXxJmMyX6X6J+xc6c//YEWvpeif8cAhWA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + '@typescript-eslint/parser@8.54.0': resolution: {integrity: sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4154,6 +4823,10 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/scope-manager@6.18.1': + resolution: {integrity: sha512-BgdBwXPFmZzaZUuw6wKiHKIovms97a7eTImjkXCZE04TGHysG+0hDQPmygyvgtkoB/aOQwSM/nWv3LzrOIQOBw==} + engines: {node: ^16.0.0 || >=18.0.0} + '@typescript-eslint/scope-manager@8.54.0': resolution: {integrity: sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4164,6 +4837,16 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/type-utils@6.18.1': + resolution: {integrity: sha512-wyOSKhuzHeU/5pcRDP2G2Ndci+4g653V43gXTpt4nbyoIOAASkGDA9JIAgbQCdCkcr1MvpSYWzxTz0olCn8+/Q==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + '@typescript-eslint/type-utils@8.54.0': resolution: {integrity: sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4171,16 +4854,35 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/types@6.18.1': + resolution: {integrity: sha512-4TuMAe+tc5oA7wwfqMtB0Y5OrREPF1GeJBAjqwgZh1lEMH5PJQgWgHGfYufVB51LtjD+peZylmeyxUXPfENLCw==} + engines: {node: ^16.0.0 || >=18.0.0} + '@typescript-eslint/types@8.54.0': resolution: {integrity: sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@6.18.1': + resolution: {integrity: sha512-fv9B94UAhywPRhUeeV/v+3SBDvcPiLxRZJw/xZeeGgRLQZ6rLMG+8krrJUyIf6s1ecWTzlsbp0rlw7n9sjufHA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + '@typescript-eslint/typescript-estree@8.54.0': resolution: {integrity: sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/utils@6.18.1': + resolution: {integrity: sha512-zZmTuVZvD1wpoceHvoQpOiewmWu3uP9FuTWo8vqpy2ffsmfCE8mklRPi+vmnIYAIk9t/4kOThri2QCDgor+OpQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + '@typescript-eslint/utils@8.54.0': resolution: {integrity: sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4188,6 +4890,10 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/visitor-keys@6.18.1': + resolution: {integrity: sha512-/kvt0C5lRqGoCfsbmm7/CwMqoSkY3zzHLIjdhHZQW3VFrnz7ATecOHR7nb7V+xn4286MBxfnQfQhAmCI0u+bJA==} + engines: {node: ^16.0.0 || >=18.0.0} + '@typescript-eslint/visitor-keys@8.54.0': resolution: {integrity: sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4475,6 +5181,88 @@ packages: peerDependencies: vue: ^3.5.0 + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + + '@webpack-cli/configtest@2.1.1': + resolution: {integrity: sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==} + engines: {node: '>=14.15.0'} + peerDependencies: + webpack: 5.x.x + webpack-cli: 5.x.x + + '@webpack-cli/info@2.0.2': + resolution: {integrity: sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==} + engines: {node: '>=14.15.0'} + peerDependencies: + webpack: 5.x.x + webpack-cli: 5.x.x + + '@webpack-cli/serve@2.0.5': + resolution: {integrity: sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==} + engines: {node: '>=14.15.0'} + peerDependencies: + webpack: 5.x.x + webpack-cli: 5.x.x + webpack-dev-server: '*' + peerDependenciesMeta: + webpack-dev-server: + optional: true + + '@wojtekmaj/date-utils@1.5.1': + resolution: {integrity: sha512-+i7+JmNiE/3c9FKxzWFi2IjRJ+KzZl1QPu6QNrsgaa2MuBgXvUy4gA1TVzf/JMdIIloB76xSKikTWuyYAIVLww==} + + '@xobotyi/scrollbar-width@1.9.5': + resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==} + + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -4483,6 +5271,12 @@ packages: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} + acorn-import-phases@1.0.4: + resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} + engines: {node: '>=10.13.0'} + peerDependencies: + acorn: ^8.14.0 + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -4502,10 +5296,24 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + add-dom-event-listener@1.1.0: + resolution: {integrity: sha512-WCxx1ixHT0GQU9hb0KI/mhgRQhnU+U3GvwY6ZvVjYq8rsihIGoaIOUbY0yMPBxLH5MDtr0kz3fisWGNcbWW7Jw==} + + add-px-to-style@1.0.0: + resolution: {integrity: sha512-YMyxSlXpPjD8uWekCQGuN40lV4bnZagUwqa2m/uFv1z/tNImSk9fnXVMUI5qwME/zzI3MMQRvjZ+69zyfSSyew==} + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv-formats@3.0.1: resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: @@ -4514,6 +5322,11 @@ packages: ajv: optional: true + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -4548,6 +5361,9 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} + ansicolor@1.1.100: + resolution: {integrity: sha512-Jl0pxRfa9WaQVUX57AB8/V2my6FJxrOR1Pp2qqFbig20QB4HzUoQ48THTKAgHlUCJeQm/s2WoOPcoIDhyCL/kw==} + ansis@3.17.0: resolution: {integrity: sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==} engines: {node: '>=14'} @@ -4587,6 +5403,9 @@ packages: resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} engines: {node: '>= 0.4'} + array-tree-filter@2.1.0: + resolution: {integrity: sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -4603,6 +5422,10 @@ packages: resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} engines: {node: '>= 0.4'} + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + arraybuffer.prototype.slice@1.0.4: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} @@ -4634,6 +5457,10 @@ packages: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} + attr-accept@2.2.5: + resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==} + engines: {node: '>=4'} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -4652,6 +5479,13 @@ packages: react-native-b4a: optional: true + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + + babel-runtime@6.26.0: + resolution: {integrity: sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -4765,12 +5599,20 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.28.6: + resolution: {integrity: sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -4821,6 +5663,9 @@ packages: resolution: {integrity: sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==} engines: {node: '>=14.16'} + calculate-size@1.1.1: + resolution: {integrity: sha512-jJZ7pvbQVM/Ss3VO789qpsypN3xmnepg242cejOAslsmlZLYw2dnj7knnNowabQ0Kzabzx56KFTy2Pot/y6FmA==} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -4829,6 +5674,10 @@ packages: resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} engines: {node: '>= 0.4'} + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + call-bound@1.0.4: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} @@ -4847,6 +5696,9 @@ packages: caniuse-lite@1.0.30001754: resolution: {integrity: sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==} + caniuse-lite@1.0.30001805: + resolution: {integrity: sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==} + capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} @@ -4909,6 +5761,10 @@ packages: chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -4920,6 +5776,9 @@ packages: class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + clean-regexp@1.0.0: resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} engines: {node: '>=4'} @@ -4949,6 +5808,9 @@ packages: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} @@ -4960,6 +5822,10 @@ packages: resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} engines: {node: '>=20'} + clone-deep@4.0.1: + resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} + engines: {node: '>=6'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -5013,6 +5879,10 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} @@ -5021,13 +5891,30 @@ packages: resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} engines: {node: '>=18'} + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + comment-parser@1.4.0: + resolution: {integrity: sha512-QLyTNiZ2KDOibvFPlZ6ZngVsZ/0gYnE6uTXi5aoDg8ed3AkJAz4sEje3Y8a29hQ1s6A99MZXe47fLAXQ1rTqaw==} + engines: {node: '>= 12.0.0'} + comment-parser@1.4.1: resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==} engines: {node: '>= 12.0.0'} + component-classes@1.2.6: + resolution: {integrity: sha512-hPFGULxdwugu1QWW3SvVOCUHLzO34+a2J6Wqy0c5ASQkfi9/8nZcBB0ZohaEbXOQlCflMAEMmEWk7u7BVs4koA==} + component-emitter@1.3.1: resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + component-indexof@0.0.3: + resolution: {integrity: sha512-puDQKvx/64HZXb4hBwIcvQLaLgux8o1CbWl39s41hrIIZDl1lJiD5jc22gj3RBeGK0ovxALDYpIbyjqDUUl0rw==} + compressible@2.0.18: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} engines: {node: '>= 0.6'} @@ -5068,6 +5955,9 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -5093,9 +5983,22 @@ packages: cookiejar@2.1.4: resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + copy-to-clipboard@3.3.3: + resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} + + copy-webpack-plugin@12.0.2: + resolution: {integrity: sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==} + engines: {node: '>= 18.12.0'} + peerDependencies: + webpack: ^5.1.0 + core-js-compat@3.46.0: resolution: {integrity: sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==} + core-js@2.6.12: + resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} + deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -5103,6 +6006,10 @@ packages: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} @@ -5110,16 +6017,179 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-animation@1.6.1: + resolution: {integrity: sha512-/48+/BaEaHRY6kNQ2OIPzKf9A6g8WjZYjhiNDNuIVbsm5tXCGIAsHDjB4Xu1C4vXJtUWZo26O68OQkDpNBaPog==} + + css-box-model@1.2.1: + resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==} + + css-in-js-utils@3.1.0: + resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==} + + css-loader@6.11.0: + resolution: {integrity: sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==} + engines: {node: '>= 12.13.0'} + peerDependencies: + '@rspack/core': 0.x || 1.x + webpack: ^5.0.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + webpack: + optional: true + css-select@5.2.2: resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + css-tree@1.1.3: + resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} + engines: {node: '>=8.0.0'} + css-what@6.2.2: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} + csscolorparser@1.0.3: + resolution: {integrity: sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + cssfilter@0.0.10: + resolution: {integrity: sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.8.5: + resolution: {integrity: sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==} + engines: {node: '>=12'} + data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} @@ -5139,6 +6209,9 @@ packages: dataloader@1.4.0: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} + date-fns@3.3.1: + resolution: {integrity: sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==} + dateformat@4.6.3: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} @@ -5171,6 +6244,9 @@ packages: resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} engines: {node: '>=10'} + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -5210,6 +6286,9 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -5251,10 +6330,6 @@ packages: dezalgo@1.0.4: resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} - diff@4.0.4: - resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} - engines: {node: '>=0.3.1'} - diff@7.0.0: resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==} engines: {node: '>=0.3.1'} @@ -5267,10 +6342,27 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} + direction@0.1.5: + resolution: {integrity: sha512-HceXsAluGbXKCz2qCVbXFUH4Vn4eNMWxY5gzydMFMnS1zKSwvDASqLwcrYLIFDpwuZ63FUAqjDLEP1eicHt8DQ==} + hasBin: true + doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dom-align@1.12.4: + resolution: {integrity: sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==} + + dom-css@2.1.0: + resolution: {integrity: sha512-w9kU7FAbaSh3QKijL6n59ofAhkkmMJ31GclJIz/vyQdjogfyxcB6Zf8CZyibOERI5o0Hxz30VmJS7+7r5fEj2Q==} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} @@ -5281,6 +6373,9 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} + dompurify@3.4.12: + resolution: {integrity: sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==} + domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -5295,6 +6390,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + earcut@2.2.4: + resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -5316,6 +6414,9 @@ packages: electron-to-chromium@1.5.250: resolution: {integrity: sha512-/5UMj9IiGDMOFBnN4i7/Ry5onJrAGSbOGo3s9FEKmwobGq6xw832ccET0CE3CkkMBZ8GJSlUIesZofpyurqDXw==} + electron-to-chromium@1.5.389: + resolution: {integrity: sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==} + emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -5349,6 +6450,10 @@ packages: resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} + enhanced-resolve@5.24.2: + resolution: {integrity: sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==} + engines: {node: '>=10.13.0'} + enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} @@ -5365,6 +6470,11 @@ packages: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} + envinfo@7.21.0: + resolution: {integrity: sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==} + engines: {node: '>=4'} + hasBin: true + environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -5372,10 +6482,17 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + es-abstract@1.24.0: resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} engines: {node: '>= 0.4'} + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} + engines: {node: '>= 0.4'} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -5384,6 +6501,13 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-iterator-helpers@1.4.0: + resolution: {integrity: sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -5450,6 +6574,12 @@ packages: peerDependencies: eslint: '>=7.0.0' + eslint-config-prettier@8.8.0: + resolution: {integrity: sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + eslint-config-xo-space@0.35.0: resolution: {integrity: sha512-+79iVcoLi3PvGcjqYDpSPzbLfqYpNcMlhsCBRsnmDoHAn4npJG6YxmHpelQKpXM7v/EeZTUKb4e1xotWlei8KA==} engines: {node: '>=12'} @@ -5532,6 +6662,12 @@ packages: '@typescript-eslint/parser': optional: true + eslint-plugin-jsdoc@46.8.2: + resolution: {integrity: sha512-5TSnD018f3tUJNne4s4gDWQflbsgOycIKEUBoCLn6XtBMgNHxQFmV8vVxUtiPxAQq8lrX85OaSG/2gnctxw9uQ==} + engines: {node: '>=16'} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + eslint-plugin-jsdoc@50.8.0: resolution: {integrity: sha512-UyGb5755LMFWPrZTEqqvTJ3urLz1iqj+bYOHFNag+sw3NvaMWP9K2z+uIn37XfNALmQLQyrBlJ5mkiVPL7ADEg==} engines: {node: '>=18'} @@ -5576,6 +6712,18 @@ packages: eslint-config-prettier: optional: true + eslint-plugin-react-hooks@4.6.0: + resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + + eslint-plugin-react@7.33.2: + resolution: {integrity: sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + eslint-plugin-unicorn@48.0.1: resolution: {integrity: sha512-FW+4r20myG/DqFcCSzoumaddKBicIPeFnTrifon2mWIzlfyvzwyqZjqVP7m4Cqr/ZYisS2aiLghkUWaPg6vtCw==} engines: {node: '>=16'} @@ -5588,6 +6736,14 @@ packages: peerDependencies: eslint: '>=8.56.0' + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-scope@8.4.0: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5618,6 +6774,12 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint@8.52.0: + resolution: {integrity: sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + eslint@9.39.1: resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5632,6 +6794,10 @@ packages: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -5645,6 +6811,14 @@ packages: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} + esrever@0.2.0: + resolution: {integrity: sha512-1e9YJt6yQkyekt2BUjTky7LZWWVyC2cIpgdnsTAvMcnzXIZvlW/fTMPkxBcZoYhgih4d+EC+iw+yv9GIkz7vrw==} + hasBin: true + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + estraverse@5.3.0: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} @@ -5663,9 +6837,16 @@ packages: eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + events-universal@1.0.1: resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + eventsource-parser@3.0.6: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} @@ -5678,6 +6859,9 @@ packages: resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} engines: {node: ^18.19.0 || >=20.5.0} + exenv@1.2.2: + resolution: {integrity: sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==} + expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -5734,6 +6918,9 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-shallow-equal@1.0.0: + resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==} + fast-uri@3.1.2: resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} @@ -5744,10 +6931,16 @@ packages: resolution: {integrity: sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==} hasBin: true + fast_array_intersect@1.1.0: + resolution: {integrity: sha512-/DCilZlUdz2XyNDF+ASs0PwY+RKG9Y4Silp/gbS72Cvbg4oibc778xcecg+pnNyiNHYgh/TApsiDTjpdniyShw==} + fastest-levenshtein@1.0.16: resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} engines: {node: '>= 4.9.1'} + fastest-stable-stringify@2.0.2: + resolution: {integrity: sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -5774,10 +6967,18 @@ packages: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-selector@0.6.0: + resolution: {integrity: sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==} + engines: {node: '>= 12'} + filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} @@ -5793,6 +6994,9 @@ packages: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -5804,6 +7008,10 @@ packages: find-yarn-workspace-root@2.0.0: resolution: {integrity: sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==} + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -5907,10 +7115,17 @@ packages: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} + geotiff@2.1.3: + resolution: {integrity: sha512-PT6uoF5a1+kbC3tHmZSUsLHBp2QJlHasxxxxPW47QIY1VBKpFB+FcDvX+MxER6UzgLQZ0xDzJ9s48B9JbOCTqA==} + engines: {node: '>=10.19'} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-document@1.0.0: + resolution: {integrity: sha512-8E7H2Xxibav+/rQTTtm6gFlSQwDoAQg667yheA+vWQr/amxEuswChzGo4MIbOJJoR0SMpDyhbUqWp3FpIfwD9A==} + get-east-asian-width@1.4.0: resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} @@ -5953,6 +7168,12 @@ packages: get-tsconfig@4.13.0: resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + get-user-locale@2.3.2: + resolution: {integrity: sha512-O2GWvQkhnbDoWFUJfaBlDIKUEdND8ATpBXD6KXcbhxlfktyD/d8w6mkzM/IlQEqGZAMz/PW6j6Hv53BiigKLUQ==} + + get-window@1.1.2: + resolution: {integrity: sha512-yjWpFcy9fjhLQHW1dPtg9ga4pmizLY8y4ZSHdGrAQ1NU277MRhnGnnLPxe19X8W5lWVsCZz++5xEuNozWMVmTw==} + git-hooks-list@3.2.0: resolution: {integrity: sha512-ZHG9a1gEhUMX1TvGrLdyWb9kDopCBbTnI8z4JgRMYxsijWipgjSEYoPWqBuIB0DnRnvqlQSEeVmzpeuPm7NdFQ==} @@ -6038,6 +7259,9 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + graphql@16.12.0: resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} @@ -6092,9 +7316,18 @@ packages: help-me@5.0.0: resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + highlight-words-core@1.2.3: + resolution: {integrity: sha512-m1O9HW3/GNHxzSIXWw1wCNXXsgLlxrP0OI6+ycGUhiUHkikqW3OrwVHz+lxeNBe5yqLESdIcj8PowHQ2zLvUvQ==} + highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + history@4.10.1: + resolution: {integrity: sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==} + + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + hono@4.12.26: resolution: {integrity: sha512-uyZtpnYxM9CmQ7QsQknM4zN8EftNqhON1qYeIKM0Se67CCEe2c44xyGURwB0axX2fBDu1dqHrHAc1hmNT8ITkw==} engines: {node: '>=16.9.0'} @@ -6116,6 +7349,9 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} @@ -6165,6 +7401,15 @@ packages: resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} engines: {node: '>=18.18.0'} + hyphenate-style-name@1.1.0: + resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==} + + i18next-browser-languagedetector@7.2.2: + resolution: {integrity: sha512-6b7r75uIJDWCcCflmbof+sJ94k9UQO4X0YR62oUfqGI/GjCLVzlCwu8TFdRZIqVLzWbzNcmkmhfqKEr4TLz4HQ==} + + i18next@23.16.8: + resolution: {integrity: sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==} + i18next@25.7.4: resolution: {integrity: sha512-hRkpEblXXcXSNbw8mBNq9042OEetgyB/ahc/X17uV/khPwzV+uB8RHceHh3qavyrkPJvmXFKXME2Sy1E0KjAfw==} peerDependencies: @@ -6185,6 +7430,12 @@ packages: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} + icss-utils@5.1.0: + resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -6199,10 +7450,18 @@ packages: immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + immutable@4.3.5: + resolution: {integrity: sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -6225,14 +7484,31 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + inline-style-prefixer@7.0.1: + resolution: {integrity: sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + interpret@1.4.0: resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} engines: {node: '>= 0.10'} + interpret@3.1.1: + resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} + engines: {node: '>=10.13.0'} + + intl-messageformat@10.7.18: + resolution: {integrity: sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==} + + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + ip-address@10.2.0: resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} engines: {node: '>= 12'} @@ -6279,6 +7555,10 @@ packages: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + is-data-view@1.0.2: resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} engines: {node: '>= 0.4'} @@ -6317,6 +7597,15 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hotkey@0.1.4: + resolution: {integrity: sha512-Py+aW4r5mBBY18TGzGz286/gKS+fCQ0Hee3qkaiSmEPiD0PqFpe0wuA3l7rTOUKyeXl8Mxf3XzJxIoTlSv+kxA==} + + is-hotkey@0.2.0: + resolution: {integrity: sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==} + + is-in-browser@1.1.3: + resolution: {integrity: sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==} + is-in-ssh@1.0.0: resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} engines: {node: '>=20'} @@ -6361,6 +7650,10 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + is-plain-object@5.0.0: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} @@ -6432,6 +7725,9 @@ packages: resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} engines: {node: '>= 0.4'} + is-window@1.0.2: + resolution: {integrity: sha512-uj00kdXyZb9t9RcAUAwMZAnkBUwdYGhYlt7djMXhfyhUCzwNba50tIiBKR7q0l7tdoBtFVw/3JmLY6fI3rmZmg==} + is-windows@1.0.2: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} @@ -6444,6 +7740,9 @@ packages: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} + isarray@0.0.1: + resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -6457,6 +7756,13 @@ packages: resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} engines: {node: '>=16'} + isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + + isomorphic-base64@1.0.2: + resolution: {integrity: sha512-pQFyLwShVPA1Qr0dE1ZPguJkbOsFGDfSq6Wzz6XaO33v74X6/iQjgYPozwkeKGQxOI1/H3Fz7+ROtnV1veyKEg==} + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -6473,6 +7779,10 @@ packages: resolution: {integrity: sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==} engines: {node: '>=4'} + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -6485,6 +7795,10 @@ packages: engines: {node: '>=10'} hasBin: true + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + jose@6.1.3: resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} @@ -6492,6 +7806,12 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + jquery@3.7.1: + resolution: {integrity: sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==} + + js-cookie@2.2.1: + resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==} + js-levenshtein@1.1.6: resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} engines: {node: '>=0.10.0'} @@ -6507,6 +7827,10 @@ packages: resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} hasBin: true + jsdoc-type-pratt-parser@4.0.0: + resolution: {integrity: sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==} + engines: {node: '>=12.0.0'} + jsdoc-type-pratt-parser@4.1.0: resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==} engines: {node: '>=12.0.0'} @@ -6541,6 +7865,9 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json-stringify-pretty-compact@2.0.0: + resolution: {integrity: sha512-WRitRfs6BGq4q8gTgOy4ek7iPFXjbra0H3PmDLKm2xnZ+Gh1HUhiKGgCZkSPNULlP7mvfu6FV/mOLhCarspADQ==} + json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -6566,6 +7893,10 @@ packages: resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} engines: {node: '>=12', npm: '>=6'} + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} @@ -6584,9 +7915,16 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + kuler@2.0.0: resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + lerc@3.0.0: + resolution: {integrity: sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==} + leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -6608,6 +7946,10 @@ packages: linkify-it@5.0.0: resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + loader-runner@4.3.2: + resolution: {integrity: sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==} + engines: {node: '>=6.11.5'} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -6707,6 +8049,13 @@ packages: make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + map-age-cleaner@0.1.3: + resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==} + engines: {node: '>=6'} + + mapbox-to-css-font@2.4.5: + resolution: {integrity: sha512-VJ6nB8emkO9VODI0Fk+TQ/0zKBTqmf/Pkt8Xv0kHstoc0iXRajA00DAid4Kc3K5xeFIOoiZrVxijEzj0GLVO2w==} + mark.js@8.11.1: resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} @@ -6714,12 +8063,22 @@ packages: resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} hasBin: true + marked-mangle@1.1.7: + resolution: {integrity: sha512-bLsXKovJEEs/Dl++TBPmjX8ALFmrH5G0doTs+BdDOloBKWYRf3acyJghce78SnwInDkNPJ6crubr4MnFG7urOA==} + peerDependencies: + marked: '>=4 <13' + marked-terminal@7.3.0: resolution: {integrity: sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw==} engines: {node: '>=16.0.0'} peerDependencies: marked: '>=1 <16' + marked@12.0.0: + resolution: {integrity: sha512-Vkwtq9rLqXryZnWaQc86+FHLC6tr/fycMfYAhiOIXkrNmeGAyhSxjqu0Rs1i0bBqw5u0S7+lV9fdH2ZSVaoa0w==} + engines: {node: '>= 18'} + hasBin: true + marked@15.0.12: resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==} engines: {node: '>= 18'} @@ -6732,6 +8091,9 @@ packages: mdast-util-to-hast@13.2.1: resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + mdn-data@2.0.14: + resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + mdn-data@2.23.0: resolution: {integrity: sha512-786vq1+4079JSeu2XdcDjrhi/Ry7BWtjDl9WtGPWLiIHb2T66GvIVflZTBoSNZ5JqTtJGYEVMuFA/lbQlMOyDQ==} @@ -6746,6 +8108,19 @@ packages: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} + mem@8.1.1: + resolution: {integrity: sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==} + engines: {node: '>=10'} + + memoize-one@4.0.3: + resolution: {integrity: sha512-QmpUu4KqDmX0plH4u+tf0riMc1KHE1+lw95cMrLlXQAFOx/xnBtwhZ52XJxd9X2O6kwKBqX32kmhbhlobD0cuw==} + + memoize-one@5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} @@ -6753,6 +8128,9 @@ packages: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -6761,6 +8139,9 @@ packages: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} + micro-memoize@4.2.0: + resolution: {integrity: sha512-dRxIsNh0XosO9sd3aASUabKOzG9dloLO41g74XUGThpHBoGm1ttakPT5in14CuW/EDedkniaShFHbymmmKGOQA==} + micromark-util-character@2.1.1: resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} @@ -6818,6 +8199,10 @@ packages: engines: {node: '>=4.0.0'} hasBin: true + mimic-fn@3.1.0: + resolution: {integrity: sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==} + engines: {node: '>=8'} + mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} @@ -6834,6 +8219,13 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} + mini-create-react-context@0.4.1: + resolution: {integrity: sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + prop-types: ^15.0.0 + react: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + minimatch@10.2.4: resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} engines: {node: 18 || 20 || >=22} @@ -6852,6 +8244,49 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minimizer-webpack-plugin@5.6.1: + resolution: {integrity: sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@minify-html/node': '*' + '@swc/core': '*' + '@swc/css': '*' + '@swc/html': '*' + clean-css: '*' + cssnano: '*' + csso: '*' + esbuild: '*' + html-minifier-terser: '*' + lightningcss: '*' + postcss: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@minify-html/node': + optional: true + '@swc/core': + optional: true + '@swc/css': + optional: true + '@swc/html': + optional: true + clean-css: + optional: true + cssnano: + optional: true + csso: + optional: true + esbuild: + optional: true + html-minifier-terser: + optional: true + lightningcss: + optional: true + postcss: + optional: true + uglify-js: + optional: true + minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} @@ -6877,6 +8312,15 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true + moment-timezone@0.5.45: + resolution: {integrity: sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==} + + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + + monaco-editor@0.34.0: + resolution: {integrity: sha512-VF+S5zG8wxfinLKLrWcl4WUizMx+LeJrG4PM/M78OhcwocpV0jiyhX/pG6Q9jIOhrb/ckYi6nHnaR5OojlOZCQ==} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -6911,6 +8355,12 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nano-css@5.6.2: + resolution: {integrity: sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw==} + peerDependencies: + react: '*' + react-dom: '*' + nanoid@3.3.14: resolution: {integrity: sha512-U9kYi5bpVMEI31yC8iw4bJJp0avcHXA0W8/wNfLfnvJYzihQo2ZRPYPvpAAd570HAcCBjCTN7vnr+v4StKl1IQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -6939,6 +8389,9 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + nise@6.1.5: resolution: {integrity: sha512-SnRDPDBjxZZoU2n0+gzzLtSvo1OZo7j6jnbXsoh3AFxEGhaFU7ZF0TmefuKERq79wxR2U+MPn7ArW+Tl+clC3A==} @@ -6961,6 +8414,10 @@ packages: resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} engines: {node: '>=18'} + node-exports-info@1.6.2: + resolution: {integrity: sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==} + engines: {node: '>= 0.4'} + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -6977,6 +8434,10 @@ packages: node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + node-releases@2.0.51: + resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} + engines: {node: '>=18'} + node-sarif-builder@3.4.0: resolution: {integrity: sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==} engines: {node: '>=20'} @@ -7105,6 +8566,10 @@ packages: resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} engines: {node: '>= 0.4'} + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + object.fromentries@2.0.8: resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} engines: {node: '>= 0.4'} @@ -7113,6 +8578,10 @@ packages: resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} engines: {node: '>= 0.4'} + object.hasown@1.1.4: + resolution: {integrity: sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==} + engines: {node: '>= 0.4'} + object.values@1.2.1: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} @@ -7125,6 +8594,12 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + ol-mapbox-style@10.7.0: + resolution: {integrity: sha512-S/UdYBuOjrotcR95Iq9AejGYbifKeZE85D9VtH11ryJLQPTZXZSW1J5bIXcr4AlAH6tyjPPHTK34AdkwB32Myw==} + + ol@7.4.0: + resolution: {integrity: sha512-bgBbiah694HhC0jt8ptEFNRXwgO8d6xWH3G97PCg4bmn9Li5nLLbi59oSrvqUI6VPVwonPQF1YcqJymxxyMC6A==} + on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} @@ -7191,6 +8666,10 @@ packages: resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} engines: {node: '>=12.20'} + p-defer@1.0.0: + resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==} + engines: {node: '>=4'} + p-filter@2.1.0: resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} engines: {node: '>=8'} @@ -7235,6 +8714,12 @@ packages: pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + pako@2.2.0: + resolution: {integrity: sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==} + + papaparse@5.4.1: + resolution: {integrity: sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==} + param-case@3.0.4: resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} @@ -7242,6 +8727,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-headers@2.0.6: + resolution: {integrity: sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==} + parse-imports-exports@0.2.4: resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==} @@ -7339,6 +8827,9 @@ packages: path-to-regexp@0.1.13: resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} + path-to-regexp@1.9.0: + resolution: {integrity: sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==} + path-to-regexp@3.3.0: resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} @@ -7362,12 +8853,19 @@ packages: pathval@1.1.1: resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + pbf@3.2.1: + resolution: {integrity: sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==} + hasBin: true + pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} perfect-debounce@2.1.0: resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -7405,6 +8903,10 @@ packages: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -7419,6 +8921,37 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} + postcss-modules-extract-imports@3.1.0: + resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-local-by-default@4.2.0: + resolution: {integrity: sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-scope@3.2.1: + resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-values@4.0.0: + resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-selector-parser@7.1.4: + resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + postcss@8.5.15: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} @@ -7433,6 +8966,9 @@ packages: deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true + prefix-style@2.0.1: + resolution: {integrity: sha512-gdr1MBNVT0drzTq95CbSNdsrBDoHGlb2aDJP/FoY+1e+jSDPOb1Cv554gH2MGiSr2WTcXi/zu+NaFzfcHQkfBQ==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -7455,6 +8991,10 @@ packages: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} + prismjs@1.29.0: + resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} + engines: {node: '>=6'} + prismjs@1.30.0: resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} @@ -7469,6 +9009,9 @@ packages: process-warning@5.0.0: resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} @@ -7479,6 +9022,9 @@ packages: resolution: {integrity: sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==} engines: {node: '>=12.0.0'} + protocol-buffers-schema@3.6.1: + resolution: {integrity: sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -7511,6 +9057,19 @@ packages: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} + quick-lru@6.1.2: + resolution: {integrity: sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==} + engines: {node: '>=12'} + + quickselect@2.0.0: + resolution: {integrity: sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==} + + raf-schd@4.0.3: + resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} + + raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + rambda@7.5.0: resolution: {integrity: sha512-y/M9weqWAH4iopRd7EHDEQQvpFPHj1AA3oHozE9tfITHUtTR7Z9PSlIRRG2l1GuW7sefC1cXFfIcF+cgnShdBA==} @@ -7530,18 +9089,234 @@ packages: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} + rbush@3.0.1: + resolution: {integrity: sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==} + + rc-align@2.4.5: + resolution: {integrity: sha512-nv9wYUYdfyfK+qskThf4BQUSIadeI/dCsfaMZfNEoxm9HwOIioQ+LyqmMK6jWHAZQgOzMLaqawhuBXlF63vgjw==} + + rc-animate@2.11.1: + resolution: {integrity: sha512-1NyuCGFJG/0Y+9RKh5y/i/AalUCA51opyyS/jO2seELpgymZm2u9QV3xwODwEuzkmeQ1BDPxMLmYLcTJedPlkQ==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-cascader@3.21.2: + resolution: {integrity: sha512-J7GozpgsLaOtzfIHFJFuh4oFY0ePb1w10twqK6is3pAkqHkca/PsokbDr822KIRZ8/CK8CqevxohuPDVZ1RO/A==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + rc-config-loader@4.1.3: resolution: {integrity: sha512-kD7FqML7l800i6pS6pvLyIE2ncbk9Du8Q0gp/4hMPhJU6ZxApkoLcGD8ZeqgiAlfwZ6BlETq6qqe+12DUL207w==} + rc-drawer@6.5.2: + resolution: {integrity: sha512-QckxAnQNdhh4vtmKN0ZwDf3iakO83W9eZcSKWYYTDv4qcD2fHhRAZJJ/OE6v2ZlQ2kSqCJX5gYssF4HJFvsEPQ==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-motion@2.9.5: + resolution: {integrity: sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-overflow@1.5.0: + resolution: {integrity: sha512-Lm/v9h0LymeUYJf0x39OveU52InkdRXqnn2aYXfWmo8WdOonIKB2kfau+GF0fWq6jPgtdO9yMqveGcK6aIhJmg==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-resize-observer@1.4.3: + resolution: {integrity: sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-select@14.11.0: + resolution: {integrity: sha512-8J8G/7duaGjFiTXCBLWfh5P+KDWyA3KTlZDfV3xj/asMPqB2cmxfM+lH50wRiPIRsCQ6EbkCFBccPuaje3DHIg==} + engines: {node: '>=8.x'} + peerDependencies: + react: '*' + react-dom: '*' + + rc-slider@10.5.0: + resolution: {integrity: sha512-xiYght50cvoODZYI43v3Ylsqiw14+D7ELsgzR40boDZaya1HFa1Etnv9MDkQE8X/UrXAffwv2AcNAhslgYuDTw==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-time-picker@3.7.3: + resolution: {integrity: sha512-Lv1Mvzp9fRXhXEnRLO4nW6GLNxUkfAZ3RsiIBsWjGjXXvMNjdr4BX/ayElHAFK0DoJqOhm7c5tjmIYpEOwcUXg==} + + rc-tooltip@6.1.3: + resolution: {integrity: sha512-HMSbSs5oieZ7XddtINUddBLSVgsnlaSb3bZrzzGWjXa7/B7nNedmsuz72s7EWFEro9mNa7RyF3gOXKYqvJiTcQ==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-tree@5.8.8: + resolution: {integrity: sha512-S+mCMWo91m5AJqjz3PdzKilGgbFm7fFJRFiTDOcoRbD7UfMOPnerXwMworiga0O2XIo383UoWuEfeHs1WOltag==} + engines: {node: '>=10.x'} + peerDependencies: + react: '*' + react-dom: '*' + + rc-trigger@2.6.5: + resolution: {integrity: sha512-m6Cts9hLeZWsTvWnuMm7oElhf+03GOjOLfTuU0QmdB9ZrW7jR2IpI5rpNM7i9MvAAlMAmTx5Zr7g3uu/aMvZAw==} + + rc-util@4.21.1: + resolution: {integrity: sha512-Z+vlkSQVc1l8O2UjR3WQ+XdWlhj5q9BMQNLk2iOBch75CqPfrJyGtcWMcnhRlNuDu0Ndtt4kLVO8JI8BrABobg==} + + rc-util@5.44.4: + resolution: {integrity: sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-virtual-list@3.19.2: + resolution: {integrity: sha512-Ys6NcjwGkuwkeaWBDqfI3xWuZ7rDiQXlH1o2zLfFzATfEgXcqpk8CkgMfbJD81McqjcJVez25a3kPxCR807evA==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true + react-aria-components@1.19.0: + resolution: {integrity: sha512-2smSS5nqJ8cGYMQezuUXveZm7eMyHCqTN6mDpylQBYLYbdF5dxCCuW1DHn1VKLe1DybSfPvX/cZtJlDmvFfn8A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + react-aria@3.50.0: + resolution: {integrity: sha512-S0Os6QZk33fzUAKu1QLT9afoUaCBt1ZNdoiq0n2YMVgKIdNIQS8zxiZ8O9hYE6QyDkHKjD6q39LQZ+qaSAIgjw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + react-beautiful-dnd@13.1.1: + resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==} + deprecated: 'react-beautiful-dnd is now deprecated. Context and options: https://github.com/atlassian/react-beautiful-dnd/issues/2672' + peerDependencies: + react: ^16.8.5 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.5 || ^17.0.0 || ^18.0.0 + + react-calendar@4.8.0: + resolution: {integrity: sha512-qFgwo+p58sgv1QYMI1oGNaop90eJVKuHTZ3ZgBfrrpUb+9cAexxsKat0sAszgsizPMVo7vOXedV7Lqa0GQGMvA==} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-colorful@5.6.1: + resolution: {integrity: sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + react-custom-scrollbars-2@4.5.0: + resolution: {integrity: sha512-/z0nWAeXfMDr4+OXReTpYd1Atq9kkn4oI3qxq3iMXGQx1EEfwETSqB8HTAvg1X7dEqcCachbny1DRNGlqX5bDQ==} + peerDependencies: + react: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: react: ^18.3.1 + react-dropzone@14.2.3: + resolution: {integrity: sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==} + engines: {node: '>= 10.13'} + peerDependencies: + react: '>= 16.8 || 18.0.0' + + react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + + react-from-dom@0.6.2: + resolution: {integrity: sha512-qvWWTL/4xw4k/Dywd41RBpLQUSq97csuv15qrxN+izNeLYlD9wn5W8LspbfYe5CWbaSdkZ72BsaYBPQf2x4VbQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + react-highlight-words@0.20.0: + resolution: {integrity: sha512-asCxy+jCehDVhusNmCBoxDf2mm1AJ//D+EzDx1m5K7EqsMBIHdZ5G4LdwbSEXqZq1Ros0G0UySWmAtntSph7XA==} + peerDependencies: + react: ^0.14.0 || ^15.0.0 || ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 + + react-hook-form@7.81.0: + resolution: {integrity: sha512-ocbmr2p5KBMoAfj4WCUvped33lVi1Kd5DuDUvQDnB6VEAacOjPI/jMbtDdbhco4y9ct4xUuCmMY0b/C9L0QHjw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + + react-i18next@12.3.1: + resolution: {integrity: sha512-5v8E2XjZDFzK7K87eSwC7AJcAkcLt5xYZ4+yTPDAW1i7C93oOY1dnr4BaQM7un4Hm+GmghuiPvevWwlca5PwDA==} + peerDependencies: + i18next: '>= 19.0.0' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + + react-immutable-proptypes@2.2.0: + resolution: {integrity: sha512-Vf4gBsePlwdGvSZoLSBfd4HAP93HDauMY4fDjXhreg/vg6F3Fj/MXDNyTbltPC/xZKmZc+cjLu3598DdYK6sgQ==} + peerDependencies: + immutable: '>=3.6.2' + + react-inlinesvg@3.0.2: + resolution: {integrity: sha512-BEzkpMGQwEY68fgaouY7ZWvAUPb8jbj7dE9iDbWZxstDhMuz9qfpxNgvGSENKcDMdpq/XHduSk/LAmNKin4nKw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-lifecycles-compat@3.0.4: + resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} + + react-loading-skeleton@3.4.0: + resolution: {integrity: sha512-1oJEBc9+wn7BbkQQk7YodlYEIjgeR+GrRjD+QXkVjwZN7LGIcAFHrx4NhT7UHGBxNY1+zax3c+Fo6XQM4R7CgA==} + peerDependencies: + react: '>=16.8.0' + + react-popper@2.3.0: + resolution: {integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==} + peerDependencies: + '@popperjs/core': ^2.0.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + + react-redux@7.2.9: + resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} + peerDependencies: + react: ^16.8.3 || ^17 || ^18 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -7562,12 +9337,33 @@ packages: '@types/react': optional: true + react-router-dom@5.3.3: + resolution: {integrity: sha512-Ov0tGPMBgqmbu5CDmN++tv2HQ9HlWDuWIIqn4b88gjlAN5IHI+4ZUZRcpz9Hl0azFIwihbLDYw1OiHGRo7ZIng==} + peerDependencies: + react: '>=15' + + react-router@5.3.3: + resolution: {integrity: sha512-mzQGUvS3bM84TnbtMYR8ZjKnuPJ71IjSzR+DE6UkUqvN4czWIqEs17yLL8xkAycv4ev0AiN+IGrWu88vJs/p2w==} + peerDependencies: + react: '>=15' + + react-select@5.8.0: + resolution: {integrity: sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-simple-code-editor@0.14.1: resolution: {integrity: sha512-BR5DtNRy+AswWJECyA17qhUDvrrCZ6zXOCfkQY5zSmb96BVUbpVAv03WpcjcwtCwiLbIANx3gebHOcXYn1EHow==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' + react-stately@3.48.0: + resolution: {integrity: sha512-ImicSAG+lTotAe5izcs1fz49Zk48w7pDusqYg04WaPhCoej8BJ24soMu3iLXIrsi273s4P1gZrYGrqReMfgEEA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} @@ -7578,6 +9374,36 @@ packages: '@types/react': optional: true + react-table@7.8.0: + resolution: {integrity: sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA==} + peerDependencies: + react: ^16.8.3 || ^17.0.0-0 || ^18.0.0 + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react-universal-interface@0.6.2: + resolution: {integrity: sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==} + peerDependencies: + react: '*' + tslib: '*' + + react-use@17.5.0: + resolution: {integrity: sha512-PbfwSPMwp/hoL847rLnm/qkjg3sTRCvn6YhUZiHaUa3FA6/aNoFX79ul5Xt70O1rK+9GxSVqkY0eTwMdsR/bWg==} + peerDependencies: + react: '*' + react-dom: '*' + + react-window@1.8.10: + resolution: {integrity: sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==} + engines: {node: '>8.0.0'} + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -7629,10 +9455,23 @@ packages: resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} engines: {node: '>= 0.10'} + rechoir@0.8.0: + resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} + engines: {node: '>= 10.13.0'} + + redux@4.2.1: + resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} + regenerator-runtime@0.11.1: + resolution: {integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==} + + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + regex-recursion@6.0.2: resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} @@ -7673,9 +9512,16 @@ packages: requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resize-observer-polyfill@1.5.1: + resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} + resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -7684,14 +9530,25 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + resolve-pathname@3.0.0: + resolution: {integrity: sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve-protobuf-schema@2.1.0: + resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} + resolve@1.22.11: resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} engines: {node: '>= 0.4'} hasBin: true + resolve@2.0.0-next.7: + resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} + engines: {node: '>= 0.4'} + hasBin: true + responselike@3.0.0: resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} engines: {node: '>=14.16'} @@ -7711,6 +9568,14 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + rollup@4.62.2: resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -7720,6 +9585,9 @@ packages: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} + rtl-css-js@1.16.1: + resolution: {integrity: sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==} + run-applescript@7.1.0: resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} engines: {node: '>=18'} @@ -7727,6 +9595,12 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} @@ -7761,6 +9635,14 @@ packages: scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + schema-utils@4.3.3: + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} + engines: {node: '>= 10.13.0'} + + screenfull@5.2.0: + resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==} + engines: {node: '>=0.10.0'} + secretlint@10.2.2: resolution: {integrity: sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg==} engines: {node: '>=20.0.0'} @@ -7769,6 +9651,9 @@ packages: secure-json-parse@4.1.0: resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + selection-is-backward@1.0.0: + resolution: {integrity: sha512-C+6PCOO55NLCfS8uQjUKV/6E5XMuUcfOVsix5m0QqCCCKi495NgeQVNfWtAaD71NKHsdmFCJoXUGfir3qWdr9A==} + semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -7819,6 +9704,10 @@ packages: resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} engines: {node: '>= 0.4'} + set-harmonic-interval@1.0.1: + resolution: {integrity: sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==} + engines: {node: '>=6.9'} + set-proto@1.0.0: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} @@ -7829,6 +9718,13 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shallow-clone@3.0.1: + resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} + engines: {node: '>=8'} + + shallowequal@1.1.0: + resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -7898,6 +9794,49 @@ packages: resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} engines: {node: '>=14.16'} + slate-base64-serializer@0.2.115: + resolution: {integrity: sha512-GnLV7bUW/UQ5j7rVIxCU5zdB6NOVsEU6YWsCp68dndIjSGTGLaQv2+WwV3NcnrGGZEYe5qgo33j2QWrPws2C1A==} + peerDependencies: + slate: '>=0.32.0 <0.50.0' + + slate-dev-environment@0.2.5: + resolution: {integrity: sha512-oLD8Fclv/RqrDv6RYfN2CRzNcRXsUB99Qgcw5L/njTjxAdDPguV6edQ3DgUG9Q2pLFLhI15DwsKClzVfFzfwGQ==} + + slate-hotkeys@0.2.11: + resolution: {integrity: sha512-xhq/TlI74dRbO57O4ulGsvCcV4eaQ5nEEz9noZjeNLtNzFRd6lSgExRqAJqKGGIeJw+FnJ3OcqGvdb5CEc9/Ew==} + + slate-plain-serializer@0.7.13: + resolution: {integrity: sha512-TtrlaslxQBEMV0LYdf3s7VAbTxRPe1xaW10WNNGAzGA855/0RhkaHjKkQiRjHv5rvbRleVf7Nxr9fH+4uErfxQ==} + peerDependencies: + immutable: '>=3.8.1' + slate: '>=0.46.0 <0.50.0' + + slate-prop-types@0.5.44: + resolution: {integrity: sha512-JS0iW7uaciE/W3ADuzeN1HOnSjncQhHPXJ65nZNQzB0DF7mXVmbwQKI6cmCo/xKni7XRJT0JbWSpXFhEdPiBUA==} + peerDependencies: + immutable: '>=3.8.1' + slate: '>=0.32.0 <0.50.0' + + slate-react-placeholder@0.2.9: + resolution: {integrity: sha512-YSJ9Gb4tGpbzPje3eNKtu26hWM8ApxTk9RzjK+6zfD5V/RMTkuWONk24y6c9lZk0OAYNZNUmrnb/QZfU3j9nag==} + peerDependencies: + react: '>=16.0.0' + slate: '>=0.47.0' + slate-react: '>=0.22.0' + + slate-react@0.22.10: + resolution: {integrity: sha512-B2Ms1u/REbdd8yKkOItKgrw/tX8klgz5l5x6PP86+oh/yqmB6EHe0QyrYlQ9fc3WBlJUVTOL+nyAP1KmlKj2/w==} + peerDependencies: + immutable: '>=3.8.1 || >4.0.0-rc' + react: '>=16.6.0' + react-dom: '>=16.6.0' + slate: '>=0.47.0' + + slate@0.47.9: + resolution: {integrity: sha512-EK4O6b7lGt+g5H9PGw9O5KCM4RrOvOgE9mPi3rzQ0zDRlgAb2ga4TdpS6XNQbrsJWsc8I1fjaSsUeCqCUhhi9A==} + peerDependencies: + immutable: '>=3.8.1 || >4.0.0-rc' + slice-ansi@4.0.0: resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} engines: {node: '>=10'} @@ -7908,9 +9847,21 @@ packages: sonic-boom@4.2.0: resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + sort-asc@0.1.0: + resolution: {integrity: sha512-jBgdDd+rQ+HkZF2/OHCmace5dvpos/aWQpcxuyRs9QUbPRnkEJmYVo81PIGpjIdpOcsnJ4rGjStfDHsbn+UVyw==} + engines: {node: '>=0.10.0'} + + sort-desc@0.1.1: + resolution: {integrity: sha512-jfZacW5SKOP97BF5rX5kQfJmRVZP5/adDUTY8fCSPvNcXDVpUEe2pr/iKGlcyZzchRJZrswnp68fgk3qBXgkJw==} + engines: {node: '>=0.10.0'} + sort-object-keys@1.1.3: resolution: {integrity: sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==} + sort-object@0.3.2: + resolution: {integrity: sha512-aAQiEdqFTTdsvUFxXm3umdo04J7MRljoVGbBlkH7BgNsMvVNAJyGj7C/wV1A8wHWAJj/YikeZbfuCKqhggNWGA==} + engines: {node: '>=0.10.0'} + sort-package-json@2.15.1: resolution: {integrity: sha512-9x9+o8krTT2saA9liI4BljNjwAbvUnWf11Wq+i/iZt8nl2UGYnf3TH5uBydE7VALmP7AGwlfszuEeL8BDyb0YA==} hasBin: true @@ -7919,6 +9870,21 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.5.6: + resolution: {integrity: sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==} + engines: {node: '>=0.10.0'} + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -7953,9 +9919,24 @@ packages: stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + stack-generator@2.0.10: + resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==} + stack-trace@0.0.10: resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + stackframe@1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + + stacktrace-gps@3.1.2: + resolution: {integrity: sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==} + + stacktrace-js@2.0.2: + resolution: {integrity: sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==} + + state-local@1.0.7: + resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -7978,6 +9959,9 @@ packages: strict-event-emitter@0.5.1: resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + string-hash@1.1.3: + resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -7990,6 +9974,10 @@ packages: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + string.prototype.trim@1.2.10: resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} engines: {node: '>= 0.4'} @@ -8049,6 +10037,18 @@ packages: structured-source@4.0.0: resolution: {integrity: sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==} + style-loader@3.3.4: + resolution: {integrity: sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.0.0 + + stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + + stylis@4.4.0: + resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} + superagent@10.3.0: resolution: {integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==} engines: {node: '>=14.18.0'} @@ -8080,10 +10080,22 @@ packages: swagger-ui-dist@5.32.0: resolution: {integrity: sha512-nKZB0OuDvacB0s/lC2gbge+RigYvGRGpLLMWMFxaTUwfM+CfndVk9Th2IaTinqXiz6Mn26GK2zriCpv6/+5m3Q==} + swc-loader@0.2.7: + resolution: {integrity: sha512-nwYWw3Fh9ame3Rtm7StS9SBLpHRRnYcK7bnpF3UKZmesAK0gw2/ADvlURFAINmPvKtDLzp+GBiP9yLoEjg6S9w==} + peerDependencies: + '@swc/core': ^1.2.147 + webpack: '>=2' + synckit@0.11.13: resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} engines: {node: ^14.18.0 || >=16.0.0} + systemjs-cjs-extra@0.2.0: + resolution: {integrity: sha512-0dB6UkUNgXJ+GKt3OMONQmQV+stZPuy+0o5Bj4nP1YRtbCNtLg01sca3mSyOiBKAnqs5cjx7mTxwzomzsOFJnA==} + + systemjs@6.14.3: + resolution: {integrity: sha512-hQv45irdhXudAOr8r6SVSpJSGtogdGZUbJBRKCE5nsIS7tsxxvnIHqT4IOPWj+P+HcSzeWzHlGCGpmhPDIKe+w==} + tabbable@6.4.0: resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} @@ -8102,6 +10114,10 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + tar-fs@2.1.4: resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} @@ -8126,6 +10142,11 @@ packages: resolution: {integrity: sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==} engines: {node: '>=18'} + terser@5.49.0: + resolution: {integrity: sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==} + engines: {node: '>=10'} + hasBin: true + test-exclude@7.0.1: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} @@ -8157,9 +10178,25 @@ packages: thread-stream@3.1.0: resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + throttle-debounce@3.0.1: + resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==} + engines: {node: '>=10'} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tiny-jsonc@1.0.2: resolution: {integrity: sha512-f5QDAfLq6zIVSyCZQZhhyl0QS6MvAyTxgz4X4x3+EoCktNWEYJ6PeoEA97fyb98njpBNNi88ybpD7m+BDFXaCw==} + tiny-warning@0.0.3: + resolution: {integrity: sha512-r0SSA5Y5IWERF9Xh++tFPx0jITBgGggOsRLDWWew6YRw/C2dr4uNO1fw1vanrBmHsICmPyMLNBZboTlxUmUuaA==} + + tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + + tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + tinyexec@1.1.1: resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} engines: {node: '>=18'} @@ -8179,10 +10216,22 @@ packages: resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} engines: {node: '>=14.14'} + to-camel-case@1.0.0: + resolution: {integrity: sha512-nD8pQi5H34kyu1QDMFjzEIYqk0xa9Alt6ZfrdEMuHCFOfTLhDG5pgTu/aAM9Wt9lXILwlXmWP43b8sav0GNE8Q==} + + to-no-case@1.0.2: + resolution: {integrity: sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + to-space-case@1.0.0: + resolution: {integrity: sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA==} + + toggle-selection@1.0.6: + resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -8205,6 +10254,12 @@ packages: resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} engines: {node: '>= 14.0.0'} + ts-api-utils@1.4.3: + resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + ts-api-utils@2.4.0: resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} @@ -8216,6 +10271,9 @@ packages: peerDependencies: typescript: '>=4.0.0' + ts-easing@0.2.0: + resolution: {integrity: sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==} + ts-morph@27.0.2: resolution: {integrity: sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w==} @@ -8236,6 +10294,9 @@ packages: tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -8295,6 +10356,9 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} + type-of@2.0.1: + resolution: {integrity: sha512-39wxbwHdQ2sTiBB8wAzKfQ9GN+om8w+sjNWzr+vZJR5AMD5J+J7Yc8AtXnU9r/r2c8XiDZ/smxutDmZehX/qpQ==} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -8344,11 +10408,25 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' + typescript@5.2.2: + resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} + engines: {node: '>=14.17'} + hasBin: true + + typescript@5.3.3: + resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} + engines: {node: '>=14.17'} + hasBin: true + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true + ua-parser-js@1.0.41: + resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==} + hasBin: true + uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} @@ -8420,6 +10498,15 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uplot@1.6.30: + resolution: {integrity: sha512-48oVVRALM/128ttW19F2a2xobc2WfGdJ0VJFX00099CfqbCTuML7L2OrTKxNzeFP34eo1+yJbqFSoFAp2u28/Q==} + upper-case-first@2.0.2: resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==} @@ -8442,6 +10529,20 @@ packages: '@types/react': optional: true + use-isomorphic-layout-effect@1.2.1: + resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-memo-one@1.1.3: + resolution: {integrity: sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + use-sidecar@1.1.3: resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} engines: {node: '>=10'} @@ -8452,6 +10553,11 @@ packages: '@types/react': optional: true + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -8464,6 +10570,11 @@ packages: deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -8478,6 +10589,9 @@ packages: resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + value-equal@1.0.1: + resolution: {integrity: sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -8555,6 +10669,10 @@ packages: postcss: optional: true + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + vscode-html-languageservice@5.6.0: resolution: {integrity: sha512-FIVz83oGw2tBkOr8gQPeiREInnineCKGCz3ZD1Pi6opOuX3nSRkc4y4zLLWsuop+6ttYX//XZCI6SLzGhRzLmA==} @@ -8575,13 +10693,61 @@ packages: typescript: optional: true + warning@4.0.3: + resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + + watchpack@2.5.2: + resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==} + engines: {node: '>=10.13.0'} + web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + web-vitals@4.2.4: + resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} + + web-worker@1.5.0: + resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webpack-cli@5.1.4: + resolution: {integrity: sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==} + engines: {node: '>=14.15.0'} + hasBin: true + peerDependencies: + '@webpack-cli/generators': '*' + webpack: 5.x.x + webpack-bundle-analyzer: '*' + webpack-dev-server: '*' + peerDependenciesMeta: + '@webpack-cli/generators': + optional: true + webpack-bundle-analyzer: + optional: true + webpack-dev-server: + optional: true + + webpack-merge@5.10.0: + resolution: {integrity: sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==} + engines: {node: '>=10.0.0'} + + webpack-sources@3.5.1: + resolution: {integrity: sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==} + engines: {node: '>=10.13.0'} + + webpack@5.108.4: + resolution: {integrity: sha512-yur8LyJoeiWh47dErD+Ok7vlbmDsJ3UbbRPAoxbGJ54WpE2y5yVo5G/inUzujnYgw3tPmBRdn+G7PoxXaYC33w==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} @@ -8624,6 +10790,9 @@ packages: resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} engines: {node: '>=8'} + wildcard@2.0.1: + resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==} + winston-transport@4.9.0: resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} engines: {node: '>= 12.0.0'} @@ -8685,6 +10854,9 @@ packages: resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==} engines: {node: '>=16.0.0'} + xml-utils@1.10.2: + resolution: {integrity: sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA==} + xml2js@0.5.0: resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} engines: {node: '>=4.0.0'} @@ -8697,6 +10869,11 @@ packages: resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} engines: {node: '>=4.0'} + xss@1.0.15: + resolution: {integrity: sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==} + engines: {node: '>= 0.10.0'} + hasBin: true + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -8707,6 +10884,10 @@ packages: yaml-ast-parser@0.0.43: resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} + yaml@1.10.3: + resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} + engines: {node: '>= 6'} + yaml@2.8.1: resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} engines: {node: '>= 14.6'} @@ -8773,11 +10954,41 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zstddec@0.1.0: + resolution: {integrity: sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} snapshots: + '@adobe/react-spectrum-ui@1.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@adobe/react-spectrum-workflow@2.3.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@adobe/react-spectrum@3.47.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@internationalized/date': 3.12.2 + '@react-types/shared': 3.36.0(react@18.3.1) + '@spectrum-icons/ui': 3.7.1(@adobe/react-spectrum@3.47.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@spectrum-icons/workflow': 4.3.1(@adobe/react-spectrum@3.47.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@swc/helpers': 0.5.23 + client-only: 0.0.1 + clsx: 2.1.1 + react: 18.3.1 + react-aria: 3.50.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-aria-components: 1.19.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) + react-stately: 3.48.0(react@18.3.1) + react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + use-sync-external-store: 1.6.0(react@18.3.1) + '@antfu/install-pkg@1.1.0': dependencies: package-manager-detector: 1.6.0 @@ -10285,23 +12496,79 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-string-parser@7.29.7': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@7.29.7': {} + '@babel/parser@7.29.2': dependencies: '@babel/types': 7.29.0 + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + '@babel/runtime@7.28.4': {} + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@bcoe/v8-coverage@1.0.2': {} + '@braintree/sanitize-url@7.0.0': {} + '@changesets/apply-release-plan@7.1.1': dependencies: '@changesets/config': 3.1.4 @@ -10475,6 +12742,8 @@ snapshots: enabled: 2.0.0 kuler: 2.0.0 + '@discoveryjs/json-ext@0.5.7': {} + '@docsearch/css@4.6.2': {} '@docsearch/js@4.6.2': {} @@ -10497,9 +12766,91 @@ snapshots: tslib: 2.8.1 optional: true + '@emotion/babel-plugin@11.13.5': + dependencies: + '@babel/helper-module-imports': 7.29.7 + '@babel/runtime': 7.28.4 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.3 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + transitivePeerDependencies: + - supports-color + + '@emotion/cache@11.14.0': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + + '@emotion/css@11.11.2': + dependencies: + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + transitivePeerDependencies: + - supports-color + + '@emotion/hash@0.9.2': {} + + '@emotion/memoize@0.9.0': {} + + '@emotion/react@11.11.3(@types/react@18.3.12)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@18.3.1) + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.3.1 + hoist-non-react-statics: 3.3.2 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + transitivePeerDependencies: + - supports-color + + '@emotion/serialize@1.3.3': + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.2 + csstype: 3.2.3 + + '@emotion/sheet@1.4.0': {} + + '@emotion/unitless@0.10.0': {} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@emotion/utils@1.4.2': {} + + '@emotion/weak-memoize@0.3.1': {} + + '@emotion/weak-memoize@0.4.0': {} + + '@es-joy/jsdoccomment@0.40.1': + dependencies: + comment-parser: 1.4.0 + esquery: 1.6.0 + jsdoc-type-pratt-parser: 4.0.0 + '@es-joy/jsdoccomment@0.50.2': dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@typescript-eslint/types': 8.54.0 comment-parser: 1.4.1 esquery: 1.6.0 @@ -10736,6 +13087,11 @@ snapshots: '@esbuild/win32-x64@0.27.7': optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@8.52.0)': + dependencies: + eslint: 8.52.0 + eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.1)': dependencies: eslint: 9.39.1 @@ -10784,6 +13140,20 @@ snapshots: '@eslint/css-tree': 3.6.6 '@eslint/plugin-kit': 0.3.5 + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.4.3(supports-color@10.2.2) + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.2.0 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 @@ -10798,6 +13168,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@eslint/js@8.52.0': {} + '@eslint/js@9.39.1': {} '@eslint/json@0.13.2': @@ -10834,8 +13206,42 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@floating-ui/react@0.26.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@floating-ui/utils': 0.2.10 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tabbable: 6.4.0 + '@floating-ui/utils@0.2.10': {} + '@formatjs/ecma402-abstract@2.3.6': + dependencies: + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/intl-localematcher': 0.6.2 + decimal.js: 10.6.0 + tslib: 2.8.1 + + '@formatjs/fast-memoize@2.2.7': + dependencies: + tslib: 2.8.1 + + '@formatjs/icu-messageformat-parser@2.11.4': + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + '@formatjs/icu-skeleton-parser': 1.8.16 + tslib: 2.8.1 + + '@formatjs/icu-skeleton-parser@1.8.16': + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + tslib: 2.8.1 + + '@formatjs/intl-localematcher@0.6.2': + dependencies: + tslib: 2.8.1 + '@gerrit0/mini-shiki@3.15.0': dependencies: '@shikijs/engine-oniguruma': 3.15.0 @@ -10844,11 +13250,171 @@ snapshots: '@shikijs/types': 3.15.0 '@shikijs/vscode-textmate': 10.0.2 - '@h4ad/serverless-adapter@4.4.0(@types/aws-lambda@8.10.160)(@types/body-parser@1.19.6)(@types/cors@2.8.19)(@types/express@5.0.3)(body-parser@2.2.1)(cors@2.8.5)(express@5.1.0)(http-errors@2.0.1)': + '@grafana/data@10.4.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@braintree/sanitize-url': 7.0.0 + '@grafana/schema': 10.4.19 + '@types/d3-interpolate': 3.0.4 + '@types/string-hash': 1.1.3 + d3-interpolate: 3.0.1 + date-fns: 3.3.1 + dompurify: 3.4.12 + eventemitter3: 5.0.1 + fast_array_intersect: 1.1.0 + history: 4.10.1 + lodash: 4.18.1 + marked: 12.0.0 + marked-mangle: 1.1.7(marked@12.0.0) + moment: 2.30.1 + moment-timezone: 0.5.45 + ol: 7.4.0 + papaparse: 5.4.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-use: 17.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + regenerator-runtime: 0.14.1 + rxjs: 7.8.1 + string-hash: 1.1.3 + tinycolor2: 1.6.0 + tslib: 2.6.2 + uplot: 1.6.30 + xss: 1.0.15 + + '@grafana/e2e-selectors@10.4.19': + dependencies: + '@grafana/tsconfig': 1.2.0-rc1 + tslib: 2.6.2 + typescript: 5.3.3 + + '@grafana/eslint-config@7.0.0': + dependencies: + '@typescript-eslint/eslint-plugin': 6.18.1(@typescript-eslint/parser@6.18.1(eslint@8.52.0)(typescript@5.2.2))(eslint@8.52.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.18.1(eslint@8.52.0)(typescript@5.2.2) + eslint: 8.52.0 + eslint-config-prettier: 8.8.0(eslint@8.52.0) + eslint-plugin-jsdoc: 46.8.2(eslint@8.52.0) + eslint-plugin-react: 7.33.2(eslint@8.52.0) + eslint-plugin-react-hooks: 4.6.0(eslint@8.52.0) + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + + '@grafana/faro-core@1.19.0': dependencies: - '@types/body-parser': 1.19.6 - '@types/cors': 2.8.19 - optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.1) + + '@grafana/faro-web-sdk@1.19.0': + dependencies: + '@grafana/faro-core': 1.19.0 + ua-parser-js: 1.0.41 + web-vitals: 4.2.4 + + '@grafana/runtime@10.4.19(@react-spectrum/provider@3.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@grafana/data': 10.4.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@grafana/e2e-selectors': 10.4.19 + '@grafana/faro-web-sdk': 1.19.0 + '@grafana/schema': 10.4.19 + '@grafana/ui': 10.4.19(@react-spectrum/provider@3.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + history: 4.10.1 + lodash: 4.18.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + rxjs: 7.8.1 + systemjs: 6.14.3 + systemjs-cjs-extra: 0.2.0 + tslib: 2.6.2 + transitivePeerDependencies: + - '@react-spectrum/provider' + - '@types/react' + - react-native + - supports-color + + '@grafana/schema@10.4.19': + dependencies: + tslib: 2.6.2 + + '@grafana/tsconfig@1.2.0-rc1': {} + + '@grafana/tsconfig@2.2.0': {} + + '@grafana/ui@10.4.19(@react-spectrum/provider@3.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@emotion/css': 11.11.2 + '@emotion/react': 11.11.3(@types/react@18.3.12)(react@18.3.1) + '@floating-ui/react': 0.26.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@grafana/data': 10.4.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@grafana/e2e-selectors': 10.4.19 + '@grafana/faro-web-sdk': 1.19.0 + '@grafana/schema': 10.4.19 + '@leeoniya/ufuzzy': 1.0.14 + '@monaco-editor/react': 4.6.0(monaco-editor@0.34.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@popperjs/core': 2.11.8 + '@react-aria/dialog': 3.5.11(@react-spectrum/provider@3.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/focus': 3.16.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/overlays': 3.21.0(@react-spectrum/provider@3.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/utils': 3.23.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + ansicolor: 1.1.100 + calculate-size: 1.1.1 + classnames: 2.5.1 + d3: 7.8.5 + date-fns: 3.3.1 + hoist-non-react-statics: 3.3.2 + i18next: 23.16.8 + i18next-browser-languagedetector: 7.2.2 + immutable: 4.3.5 + is-hotkey: 0.2.0 + jquery: 3.7.1 + lodash: 4.18.1 + micro-memoize: 4.2.0 + moment: 2.30.1 + monaco-editor: 0.34.0 + ol: 7.4.0 + prismjs: 1.29.0 + rc-cascader: 3.21.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-drawer: 6.5.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-slider: 10.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-time-picker: 3.7.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-tooltip: 6.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-beautiful-dnd: 13.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-calendar: 4.8.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-colorful: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-custom-scrollbars-2: 4.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) + react-dropzone: 14.2.3(react@18.3.1) + react-highlight-words: 0.20.0(react@18.3.1) + react-hook-form: 7.81.0(react@18.3.1) + react-i18next: 12.3.1(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-inlinesvg: 3.0.2(react@18.3.1) + react-loading-skeleton: 3.4.0(react@18.3.1) + react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-router-dom: 5.3.3(react@18.3.1) + react-select: 5.8.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-table: 7.8.0(react@18.3.1) + react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-use: 17.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-window: 1.8.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rxjs: 7.8.1 + slate: 0.47.9(immutable@4.3.5) + slate-plain-serializer: 0.7.13(immutable@4.3.5)(slate@0.47.9(immutable@4.3.5)) + slate-react: 0.22.10(immutable@4.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(slate@0.47.9(immutable@4.3.5)) + tinycolor2: 1.6.0 + tslib: 2.6.2 + uplot: 1.6.30 + uuid: 9.0.1 + transitivePeerDependencies: + - '@react-spectrum/provider' + - '@types/react' + - react-native + - supports-color + + '@h4ad/serverless-adapter@4.4.0(@types/aws-lambda@8.10.160)(@types/body-parser@1.19.6)(@types/cors@2.8.19)(@types/express@5.0.3)(body-parser@2.2.1)(cors@2.8.5)(express@5.1.0)(http-errors@2.0.1)': + dependencies: + '@types/body-parser': 1.19.6 + '@types/cors': 2.8.19 + optionalDependencies: '@types/aws-lambda': 8.10.160 '@types/express': 5.0.3 body-parser: 2.2.1 @@ -10879,10 +13445,20 @@ snapshots: '@humanfs/core': 0.19.1 '@humanwhocodes/retry': 0.4.3 + '@humanwhocodes/config-array@0.11.14': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3(supports-color@10.2.2) + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + '@humanwhocodes/module-importer@1.0.1': {} '@humanwhocodes/momoa@3.3.10': {} + '@humanwhocodes/object-schema@2.0.3': {} + '@humanwhocodes/retry@0.4.3': {} '@iconify-json/logos@1.2.11': @@ -11190,6 +13766,23 @@ snapshots: optionalDependencies: '@types/node': 22.19.0 + '@internationalized/date@3.12.2': + dependencies: + '@swc/helpers': 0.5.23 + + '@internationalized/message@3.1.10': + dependencies: + '@swc/helpers': 0.5.23 + intl-messageformat: 10.7.18 + + '@internationalized/number@3.6.7': + dependencies: + '@swc/helpers': 0.5.23 + + '@internationalized/string@3.2.9': + dependencies: + '@swc/helpers': 0.5.23 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -11203,8 +13796,18 @@ snapshots: '@istanbuljs/schema@0.1.3': {} + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/trace-mapping@0.3.31': @@ -11217,6 +13820,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@leeoniya/ufuzzy@1.0.14': {} + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.28.4 @@ -11233,6 +13838,23 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 + '@mapbox/jsonlint-lines-primitives@2.0.3': {} + + '@mapbox/mapbox-gl-style-spec@13.28.0': + dependencies: + '@mapbox/jsonlint-lines-primitives': 2.0.3 + '@mapbox/point-geometry': 0.1.0 + '@mapbox/unitbezier': 0.0.0 + csscolorparser: 1.0.3 + json-stringify-pretty-compact: 2.0.0 + minimist: 1.2.8 + rw: 1.3.3 + sort-object: 0.3.2 + + '@mapbox/point-geometry@0.1.0': {} + + '@mapbox/unitbezier@0.0.0': {} + '@modelcontextprotocol/inspector-cli@0.18.0(zod@3.25.76)': dependencies: '@modelcontextprotocol/sdk': 1.26.0(zod@3.25.76) @@ -11291,7 +13913,7 @@ snapshots: - supports-color - utf-8-validate - '@modelcontextprotocol/inspector@0.18.0(@types/node@22.19.0)(@types/react-dom@18.3.1)(@types/react@18.3.12)(typescript@5.9.3)': + '@modelcontextprotocol/inspector@0.18.0(@swc/core@1.15.43(@swc/helpers@0.5.23))(@types/node@22.19.0)(@types/react-dom@18.3.1)(@types/react@18.3.12)(typescript@5.9.3)': dependencies: '@modelcontextprotocol/inspector-cli': 0.18.0(zod@3.25.76) '@modelcontextprotocol/inspector-client': 0.18.0(@types/react-dom@18.3.1)(@types/react@18.3.12) @@ -11302,7 +13924,7 @@ snapshots: open: 10.2.0 shell-quote: 1.8.4 spawn-rx: 5.1.2 - ts-node: 10.9.2(@types/node@22.19.0)(typescript@5.9.3) + ts-node: 10.9.2(@swc/core@1.15.43(@swc/helpers@0.5.23))(@types/node@22.19.0)(typescript@5.9.3) zod: 3.25.76 transitivePeerDependencies: - '@cfworker/json-schema' @@ -11338,6 +13960,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@monaco-editor/loader@1.7.0': + dependencies: + state-local: 1.0.7 + + '@monaco-editor/react@4.6.0(monaco-editor@0.34.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@monaco-editor/loader': 1.7.0 + monaco-editor: 0.34.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@mswjs/interceptors@0.40.0': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -11466,10 +14099,62 @@ snapshots: '@open-draft/until@2.1.0': {} + '@opentelemetry/api-logs@0.202.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api@1.9.1': {} + + '@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/otlp-transformer@0.202.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.202.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.1) + protobufjs: 7.6.4 + + '@opentelemetry/resources@2.0.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/sdk-logs@0.202.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.202.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/sdk-metrics@2.0.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/semantic-conventions@1.43.0': {} + '@paralleldrive/cuid2@2.3.1': dependencies: '@noble/hashes': 1.8.0 + '@petamoriken/float16@3.9.3': {} + '@pinojs/redact@0.4.0': {} '@pkgjs/parseargs@0.11.0': @@ -11489,6 +14174,8 @@ snapshots: '@pnpm/network.ca-file': 1.0.2 config-chain: 1.1.13 + '@popperjs/core@2.11.8': {} + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -11915,6 +14602,203 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@rc-component/portal@1.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@rc-component/trigger@1.18.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@rc-component/portal': 1.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-resize-observer: 1.4.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.44.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@react-aria/button@3.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@swc/helpers': 0.5.23 + react: 18.3.1 + react-aria: 3.50.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) + + '@react-aria/dialog@3.5.11(@react-spectrum/provider@3.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-aria/focus': 3.16.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/overlays': 3.32.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/utils': 3.23.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-types/dialog': 3.6.0(@react-spectrum/provider@3.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-types/shared': 3.36.0(react@18.3.1) + '@swc/helpers': 0.5.23 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@react-spectrum/provider' + + '@react-aria/dialog@3.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@swc/helpers': 0.5.23 + react: 18.3.1 + react-aria: 3.50.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) + + '@react-aria/focus@3.16.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-aria/interactions': 3.28.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/utils': 3.23.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-types/shared': 3.36.0(react@18.3.1) + '@swc/helpers': 0.5.23 + clsx: 2.1.1 + react: 18.3.1 + transitivePeerDependencies: + - react-dom + + '@react-aria/i18n@3.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@internationalized/date': 3.12.2 + '@internationalized/message': 3.1.10 + '@internationalized/string': 3.2.9 + '@swc/helpers': 0.5.23 + react: 18.3.1 + react-aria: 3.50.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) + + '@react-aria/interactions@3.28.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-types/shared': 3.36.0(react@18.3.1) + '@swc/helpers': 0.5.23 + react: 18.3.1 + react-aria: 3.50.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) + + '@react-aria/overlays@3.21.0(@react-spectrum/provider@3.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-aria/focus': 3.16.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/i18n': 3.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/interactions': 3.28.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/ssr': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/utils': 3.23.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/visually-hidden': 3.9.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-stately/overlays': 3.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-types/button': 3.16.0(@react-spectrum/provider@3.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-types/overlays': 3.10.0(@react-spectrum/provider@3.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-types/shared': 3.36.0(react@18.3.1) + '@swc/helpers': 0.5.23 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@react-spectrum/provider' + + '@react-aria/overlays@3.32.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@swc/helpers': 0.5.23 + react: 18.3.1 + react-aria: 3.50.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) + + '@react-aria/ssr@3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@swc/helpers': 0.5.23 + react: 18.3.1 + react-aria: 3.50.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) + + '@react-aria/utils@3.23.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-aria/ssr': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-stately/utils': 3.12.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-types/shared': 3.36.0(react@18.3.1) + '@swc/helpers': 0.5.23 + clsx: 2.1.1 + react: 18.3.1 + transitivePeerDependencies: + - react-dom + + '@react-aria/visually-hidden@3.9.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@swc/helpers': 0.5.23 + react: 18.3.1 + react-aria: 3.50.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) + + '@react-spectrum/button@3.18.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@adobe/react-spectrum': 3.47.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@swc/helpers': 0.5.23 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@react-spectrum/dialog@3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@adobe/react-spectrum': 3.47.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@swc/helpers': 0.5.23 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@react-spectrum/overlays@5.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@adobe/react-spectrum': 3.47.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@swc/helpers': 0.5.23 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@react-spectrum/provider@3.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@adobe/react-spectrum': 3.47.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@swc/helpers': 0.5.23 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@react-stately/overlays@3.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@swc/helpers': 0.5.23 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-stately: 3.48.0(react@18.3.1) + + '@react-stately/utils@3.12.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@swc/helpers': 0.5.23 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-stately: 3.48.0(react@18.3.1) + + '@react-types/button@3.16.0(@react-spectrum/provider@3.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-aria/button': 3.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-spectrum/button': 3.18.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-spectrum/provider': 3.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@react-types/dialog@3.6.0(@react-spectrum/provider@3.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-aria/dialog': 3.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-spectrum/dialog': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-spectrum/provider': 3.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@react-types/overlays@3.10.0(@react-spectrum/provider@3.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-aria/overlays': 3.32.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-spectrum/overlays': 5.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-spectrum/provider': 3.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-stately/overlays': 3.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-types/shared': 3.36.0(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@react-types/shared@3.36.0(react@18.3.1)': + dependencies: + react: 18.3.1 + '@redocly/ajv@8.17.1': dependencies: fast-deep-equal: 3.1.3 @@ -12787,6 +15671,23 @@ snapshots: color: 5.0.3 text-hex: 1.0.0 + '@spectrum-icons/ui@3.7.1(@adobe/react-spectrum@3.47.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@adobe/react-spectrum': 3.47.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@adobe/react-spectrum-ui': 1.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@babel/runtime': 7.28.4 + '@swc/helpers': 0.5.23 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@spectrum-icons/workflow@4.3.1(@adobe/react-spectrum@3.47.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@adobe/react-spectrum': 3.47.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@adobe/react-spectrum-workflow': 2.3.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@swc/helpers': 0.5.23 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@stylistic/eslint-plugin@3.1.0(eslint@9.39.1)(typescript@5.9.3)': dependencies: '@typescript-eslint/utils': 8.54.0(eslint@9.39.1)(typescript@5.9.3) @@ -12809,6 +15710,71 @@ snapshots: estraverse: 5.3.0 picomatch: 4.0.4 + '@swc/core-darwin-arm64@1.15.43': + optional: true + + '@swc/core-darwin-x64@1.15.43': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.15.43': + optional: true + + '@swc/core-linux-arm64-gnu@1.15.43': + optional: true + + '@swc/core-linux-arm64-musl@1.15.43': + optional: true + + '@swc/core-linux-ppc64-gnu@1.15.43': + optional: true + + '@swc/core-linux-s390x-gnu@1.15.43': + optional: true + + '@swc/core-linux-x64-gnu@1.15.43': + optional: true + + '@swc/core-linux-x64-musl@1.15.43': + optional: true + + '@swc/core-win32-arm64-msvc@1.15.43': + optional: true + + '@swc/core-win32-ia32-msvc@1.15.43': + optional: true + + '@swc/core-win32-x64-msvc@1.15.43': + optional: true + + '@swc/core@1.15.43(@swc/helpers@0.5.23)': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.27 + optionalDependencies: + '@swc/core-darwin-arm64': 1.15.43 + '@swc/core-darwin-x64': 1.15.43 + '@swc/core-linux-arm-gnueabihf': 1.15.43 + '@swc/core-linux-arm64-gnu': 1.15.43 + '@swc/core-linux-arm64-musl': 1.15.43 + '@swc/core-linux-ppc64-gnu': 1.15.43 + '@swc/core-linux-s390x-gnu': 1.15.43 + '@swc/core-linux-x64-gnu': 1.15.43 + '@swc/core-linux-x64-musl': 1.15.43 + '@swc/core-win32-arm64-msvc': 1.15.43 + '@swc/core-win32-ia32-msvc': 1.15.43 + '@swc/core-win32-x64-msvc': 1.15.43 + '@swc/helpers': 0.5.23 + + '@swc/counter@0.1.3': {} + + '@swc/helpers@0.5.23': + dependencies: + tslib: 2.8.1 + + '@swc/types@0.1.27': + dependencies: + '@swc/counter': 0.1.3 + '@szmarczak/http-timer@5.0.1': dependencies: defer-to-connect: 2.0.1 @@ -12866,7 +15832,7 @@ snapshots: '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 22.19.0 + '@types/node': 20.19.43 '@types/chai@4.3.20': {} @@ -12874,13 +15840,19 @@ snapshots: '@types/connect@3.4.38': dependencies: - '@types/node': 22.19.0 + '@types/node': 20.19.43 '@types/cookiejar@2.1.5': {} '@types/cors@2.8.19': dependencies: - '@types/node': 22.19.0 + '@types/node': 20.19.43 + + '@types/d3-color@3.1.3': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 '@types/ejs@3.1.5': {} @@ -12890,7 +15862,7 @@ snapshots: '@types/express-serve-static-core@5.1.1': dependencies: - '@types/node': 22.19.0 + '@types/node': 20.19.43 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 @@ -12911,16 +15883,23 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/hoist-non-react-statics@3.3.7(@types/react@18.3.12)': + dependencies: + '@types/react': 18.3.12 + hoist-non-react-statics: 3.3.2 + '@types/http-cache-semantics@4.0.4': {} '@types/http-errors@2.0.5': {} '@types/http-proxy@1.17.17': dependencies: - '@types/node': 22.19.0 + '@types/node': 20.19.43 '@types/istanbul-lib-coverage@2.0.6': {} + '@types/js-cookie@2.2.7': {} + '@types/js-yaml@4.0.9': {} '@types/json-schema@7.0.15': {} @@ -12950,18 +15929,24 @@ snapshots: '@types/mute-stream@0.0.4': dependencies: - '@types/node': 22.19.0 + '@types/node': 20.19.43 '@types/negotiator@0.6.4': {} '@types/node@12.20.55': {} + '@types/node@20.19.43': + dependencies: + undici-types: 6.21.0 + '@types/node@22.19.0': dependencies: undici-types: 6.21.0 '@types/normalize-package-data@2.4.4': {} + '@types/parse-json@4.0.2': {} + '@types/prop-types@15.7.15': {} '@types/qs@6.14.0': {} @@ -12972,6 +15957,17 @@ snapshots: dependencies: '@types/react': 18.3.12 + '@types/react-redux@7.1.34': + dependencies: + '@types/hoist-non-react-statics': 3.3.7(@types/react@18.3.12) + '@types/react': 18.3.12 + hoist-non-react-statics: 3.3.2 + redux: 4.2.1 + + '@types/react-transition-group@4.4.12(@types/react@18.3.12)': + dependencies: + '@types/react': 18.3.12 + '@types/react@18.3.12': dependencies: '@types/prop-types': 15.7.15 @@ -12979,18 +15975,20 @@ snapshots: '@types/sarif@2.1.7': {} + '@types/semver@7.7.1': {} + '@types/send@1.2.1': dependencies: - '@types/node': 22.19.0 + '@types/node': 20.19.43 '@types/serve-static@2.2.0': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 22.19.0 + '@types/node': 20.19.43 '@types/set-cookie-parser@2.4.10': dependencies: - '@types/node': 22.19.0 + '@types/node': 20.19.43 '@types/sinon@17.0.4': dependencies: @@ -13004,11 +16002,13 @@ snapshots: '@types/statuses@2.0.6': {} + '@types/string-hash@1.1.3': {} + '@types/superagent@8.1.10': dependencies: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 - '@types/node': 22.19.0 + '@types/node': 20.19.43 form-data: 4.0.6 '@types/supertest@6.0.3': @@ -13018,33 +16018,76 @@ snapshots: '@types/tar-fs@2.0.4': dependencies: - '@types/node': 22.19.0 + '@types/node': 20.19.43 '@types/tar-stream': 3.1.4 '@types/tar-stream@3.1.4': dependencies: - '@types/node': 22.19.0 + '@types/node': 20.19.43 '@types/triple-beam@1.3.5': {} + '@types/trusted-types@2.0.7': + optional: true + '@types/unist@3.0.3': {} '@types/vscode@1.109.0': {} '@types/web-bluetooth@0.0.21': {} + '@types/webpack@5.28.5(@swc/core@1.15.43(@swc/helpers@0.5.23))(postcss@8.5.15)(webpack-cli@5.1.4)': + dependencies: + '@types/node': 20.19.43 + tapable: 2.3.0 + webpack: 5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(postcss@8.5.15)(webpack-cli@5.1.4) + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/core' + - '@swc/css' + - '@swc/html' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - uglify-js + - webpack-cli + '@types/wrap-ansi@3.0.0': {} '@types/xml2js@0.4.14': dependencies: - '@types/node': 22.19.0 + '@types/node': 20.19.43 - '@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@6.18.1(@typescript-eslint/parser@6.18.1(eslint@8.52.0)(typescript@5.2.2))(eslint@8.52.0)(typescript@5.2.2)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.54.0(eslint@9.39.1)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.54.0 - '@typescript-eslint/type-utils': 8.54.0(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/parser': 6.18.1(eslint@8.52.0)(typescript@5.2.2) + '@typescript-eslint/scope-manager': 6.18.1 + '@typescript-eslint/type-utils': 6.18.1(eslint@8.52.0)(typescript@5.2.2) + '@typescript-eslint/utils': 6.18.1(eslint@8.52.0)(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 6.18.1 + debug: 4.4.3(supports-color@10.2.2) + eslint: 8.52.0 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + semver: 7.7.3 + ts-api-utils: 1.4.3(typescript@5.2.2) + optionalDependencies: + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.54.0(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.54.0 + '@typescript-eslint/type-utils': 8.54.0(eslint@9.39.1)(typescript@5.9.3) '@typescript-eslint/utils': 8.54.0(eslint@9.39.1)(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.54.0 eslint: 9.39.1 @@ -13055,6 +16098,19 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/parser@6.18.1(eslint@8.52.0)(typescript@5.2.2)': + dependencies: + '@typescript-eslint/scope-manager': 6.18.1 + '@typescript-eslint/types': 6.18.1 + '@typescript-eslint/typescript-estree': 6.18.1(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 6.18.1 + debug: 4.4.3(supports-color@10.2.2) + eslint: 8.52.0 + optionalDependencies: + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/parser@8.54.0(eslint@9.39.1)(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.54.0 @@ -13076,6 +16132,11 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/scope-manager@6.18.1': + dependencies: + '@typescript-eslint/types': 6.18.1 + '@typescript-eslint/visitor-keys': 6.18.1 + '@typescript-eslint/scope-manager@8.54.0': dependencies: '@typescript-eslint/types': 8.54.0 @@ -13085,6 +16146,18 @@ snapshots: dependencies: typescript: 5.9.3 + '@typescript-eslint/type-utils@6.18.1(eslint@8.52.0)(typescript@5.2.2)': + dependencies: + '@typescript-eslint/typescript-estree': 6.18.1(typescript@5.2.2) + '@typescript-eslint/utils': 6.18.1(eslint@8.52.0)(typescript@5.2.2) + debug: 4.4.3(supports-color@10.2.2) + eslint: 8.52.0 + ts-api-utils: 1.4.3(typescript@5.2.2) + optionalDependencies: + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/type-utils@8.54.0(eslint@9.39.1)(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.54.0 @@ -13097,8 +16170,25 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/types@6.18.1': {} + '@typescript-eslint/types@8.54.0': {} + '@typescript-eslint/typescript-estree@6.18.1(typescript@5.2.2)': + dependencies: + '@typescript-eslint/types': 6.18.1 + '@typescript-eslint/visitor-keys': 6.18.1 + debug: 4.4.3(supports-color@10.2.2) + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.9 + semver: 7.7.3 + ts-api-utils: 1.4.3(typescript@5.2.2) + optionalDependencies: + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/typescript-estree@8.54.0(typescript@5.9.3)': dependencies: '@typescript-eslint/project-service': 8.54.0(typescript@5.9.3) @@ -13114,6 +16204,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@6.18.1(eslint@8.52.0)(typescript@5.2.2)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.52.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.7.1 + '@typescript-eslint/scope-manager': 6.18.1 + '@typescript-eslint/types': 6.18.1 + '@typescript-eslint/typescript-estree': 6.18.1(typescript@5.2.2) + eslint: 8.52.0 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + - typescript + '@typescript-eslint/utils@8.54.0(eslint@9.39.1)(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.1) @@ -13125,6 +16229,11 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/visitor-keys@6.18.1': + dependencies: + '@typescript-eslint/types': 6.18.1 + eslint-visitor-keys: 3.4.3 + '@typescript-eslint/visitor-keys@8.54.0': dependencies: '@typescript-eslint/types': 8.54.0 @@ -13199,10 +16308,10 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitejs/plugin-vue@6.0.6(vite@7.3.5(@types/node@22.19.0)(tsx@4.20.6)(yaml@2.9.0))(vue@3.5.32(typescript@5.9.3))': + '@vitejs/plugin-vue@6.0.6(vite@7.3.5(@types/node@22.19.0)(terser@5.49.0)(tsx@4.20.6)(yaml@2.9.0))(vue@3.5.32(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.13 - vite: 7.3.5(@types/node@22.19.0)(tsx@4.20.6)(yaml@2.9.0) + vite: 7.3.5(@types/node@22.19.0)(terser@5.49.0)(tsx@4.20.6)(yaml@2.9.0) vue: 3.5.32(typescript@5.9.3) '@vscode/debugadapter@1.68.0': @@ -13402,6 +16511,105 @@ snapshots: dependencies: vue: 3.5.32(typescript@5.9.3) + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + + '@webassemblyjs/helper-api-error@1.13.2': {} + + '@webassemblyjs/helper-buffer@1.14.1': {} + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.13.2': {} + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + + '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4)(webpack@5.108.4)': + dependencies: + webpack: 5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(postcss@8.5.15)(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack@5.108.4) + + '@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.108.4)': + dependencies: + webpack: 5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(postcss@8.5.15)(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack@5.108.4) + + '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack@5.108.4)': + dependencies: + webpack: 5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(postcss@8.5.15)(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack@5.108.4) + + '@wojtekmaj/date-utils@1.5.1': {} + + '@xobotyi/scrollbar-width@1.9.5': {} + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -13412,24 +16620,47 @@ snapshots: mime-types: 3.0.1 negotiator: 1.0.0 + acorn-import-phases@1.0.4(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + acorn-walk@8.3.4: dependencies: - acorn: 8.15.0 + acorn: 8.16.0 acorn@8.15.0: {} acorn@8.16.0: {} + add-dom-event-listener@1.1.0: + dependencies: + object-assign: 4.1.1 + + add-px-to-style@1.0.0: {} + agent-base@7.1.4: {} + ajv-formats@2.1.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + ajv-formats@3.0.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 + ajv-keywords@5.1.0(ajv@8.17.1): + dependencies: + ajv: 8.17.1 + fast-deep-equal: 3.1.3 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -13464,6 +16695,8 @@ snapshots: ansi-styles@6.2.3: {} + ansicolor@1.1.100: {} + ansis@3.17.0: {} any-promise@1.3.0: {} @@ -13505,6 +16738,8 @@ snapshots: is-string: 1.1.1 math-intrinsics: 1.1.0 + array-tree-filter@2.1.0: {} + array-union@2.1.0: {} array.prototype.findlastindex@1.2.6: @@ -13531,6 +16766,14 @@ snapshots: es-abstract: 1.24.0 es-shim-unscopables: 1.1.0 + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + arraybuffer.prototype.slice@1.0.4: dependencies: array-buffer-byte-length: 1.0.2 @@ -13559,6 +16802,8 @@ snapshots: atomic-sleep@1.0.0: {} + attr-accept@2.2.5: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 @@ -13576,6 +16821,17 @@ snapshots: b4a@1.7.3: {} + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.28.4 + cosmiconfig: 7.1.0 + resolve: 1.22.11 + + babel-runtime@6.26.0: + dependencies: + core-js: 2.6.12 + regenerator-runtime: 0.11.1 + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -13710,10 +16966,20 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.1.4(browserslist@4.28.0) + browserslist@4.28.6: + dependencies: + baseline-browser-mapping: 2.9.19 + caniuse-lite: 1.0.30001805 + electron-to-chromium: 1.5.389 + node-releases: 2.0.51 + update-browserslist-db: 1.2.3(browserslist@4.28.6) + buffer-crc32@0.2.13: {} buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} + buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -13774,6 +17040,8 @@ snapshots: normalize-url: 8.1.0 responselike: 3.0.0 + calculate-size@1.1.1: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -13786,6 +17054,13 @@ snapshots: get-intrinsic: 1.3.0 set-function-length: 1.2.2 + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 @@ -13802,6 +17077,8 @@ snapshots: caniuse-lite@1.0.30001754: {} + caniuse-lite@1.0.30001805: {} + capital-case@1.0.4: dependencies: no-case: 3.0.4 @@ -13902,6 +17179,8 @@ snapshots: chownr@1.1.4: optional: true + chrome-trace-event@1.0.4: {} + ci-info@3.9.0: {} ci-info@4.3.1: {} @@ -13910,6 +17189,8 @@ snapshots: dependencies: clsx: 2.1.1 + classnames@2.5.1: {} + clean-regexp@1.0.0: dependencies: escape-string-regexp: 1.0.5 @@ -13941,6 +17222,8 @@ snapshots: cli-width@4.1.0: {} + client-only@0.0.1: {} + cliui@7.0.4: dependencies: string-width: 4.2.3 @@ -13959,6 +17242,12 @@ snapshots: strip-ansi: 7.1.2 wrap-ansi: 9.0.2 + clone-deep@4.0.1: + dependencies: + is-plain-object: 2.0.4 + kind-of: 6.0.3 + shallow-clone: 3.0.1 + clsx@2.1.1: {} cmdk@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -14008,14 +17297,28 @@ snapshots: comma-separated-tokens@2.0.3: {} + commander@10.0.1: {} + commander@12.1.0: {} commander@13.1.0: {} + commander@2.20.3: {} + + commander@7.2.0: {} + + comment-parser@1.4.0: {} + comment-parser@1.4.1: {} + component-classes@1.2.6: + dependencies: + component-indexof: 0.0.3 + component-emitter@1.3.1: {} + component-indexof@0.0.3: {} + compressible@2.0.18: dependencies: mime-db: 1.54.0 @@ -14056,6 +17359,8 @@ snapshots: content-type@1.0.5: {} + convert-source-map@1.9.0: {} + convert-source-map@2.0.0: {} cookie-signature@1.0.6: {} @@ -14070,10 +17375,26 @@ snapshots: cookiejar@2.1.4: {} + copy-to-clipboard@3.3.3: + dependencies: + toggle-selection: 1.0.6 + + copy-webpack-plugin@12.0.2(webpack@5.108.4): + dependencies: + fast-glob: 3.3.3 + glob-parent: 6.0.2 + globby: 14.1.0 + normalize-path: 3.0.0 + schema-utils: 4.3.3 + serialize-javascript: 7.0.6 + webpack: 5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(postcss@8.5.15)(webpack-cli@5.1.4) + core-js-compat@3.46.0: dependencies: browserslist: 4.28.0 + core-js@2.6.12: {} + core-util-is@1.0.3: {} cors@2.8.5: @@ -14081,6 +17402,14 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.3 + create-require@1.1.1: {} cross-spawn@7.0.6: @@ -14089,6 +17418,32 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-animation@1.6.1: + dependencies: + babel-runtime: 6.26.0 + component-classes: 1.2.6 + + css-box-model@1.2.1: + dependencies: + tiny-invariant: 1.3.3 + + css-in-js-utils@3.1.0: + dependencies: + hyphenate-style-name: 1.1.0 + + css-loader@6.11.0(webpack@5.108.4): + dependencies: + icss-utils: 5.1.0(postcss@8.5.15) + postcss: 8.5.15 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.15) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.15) + postcss-modules-scope: 3.2.1(postcss@8.5.15) + postcss-modules-values: 4.0.0(postcss@8.5.15) + postcss-value-parser: 4.2.0 + semver: 7.7.3 + optionalDependencies: + webpack: 5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(postcss@8.5.15)(webpack-cli@5.1.4) + css-select@5.2.2: dependencies: boolbase: 1.0.0 @@ -14097,10 +17452,173 @@ snapshots: domutils: 3.2.2 nth-check: 2.1.1 + css-tree@1.1.3: + dependencies: + mdn-data: 2.0.14 + source-map: 0.6.1 + css-what@6.2.2: {} + csscolorparser@1.0.3: {} + + cssesc@3.0.0: {} + + cssfilter@0.0.10: {} + csstype@3.2.3: {} + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.1.0 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.2: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.8.5: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.2 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + data-uri-to-buffer@4.0.1: {} data-view-buffer@1.0.2: @@ -14123,6 +17641,8 @@ snapshots: dataloader@1.4.0: {} + date-fns@3.3.1: {} + dateformat@4.6.3: {} debug@2.6.9: @@ -14147,6 +17667,8 @@ snapshots: decamelize@4.0.0: {} + decimal.js@10.6.0: {} + decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 @@ -14183,6 +17705,10 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + delaunator@5.1.0: + dependencies: + robust-predicates: 3.0.3 + delayed-stream@1.0.0: {} depd@2.0.0: {} @@ -14211,8 +17737,6 @@ snapshots: asap: 2.0.6 wrappy: 1.0.2 - diff@4.0.4: {} - diff@7.0.0: {} diff@8.0.3: {} @@ -14221,10 +17745,29 @@ snapshots: dependencies: path-type: 4.0.0 + direction@0.1.5: {} + doctrine@2.1.0: dependencies: esutils: 2.0.3 + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dom-align@1.12.4: {} + + dom-css@2.1.0: + dependencies: + add-px-to-style: 1.0.0 + prefix-style: 2.0.1 + to-camel-case: 1.0.0 + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.28.4 + csstype: 3.2.3 + dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 @@ -14237,6 +17780,10 @@ snapshots: dependencies: domelementtype: 2.3.0 + dompurify@3.4.12: + optionalDependencies: + '@types/trusted-types': 2.0.7 + domutils@3.2.2: dependencies: dom-serializer: 2.0.0 @@ -14256,6 +17803,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + earcut@2.2.4: {} + eastasianwidth@0.2.0: {} ecdsa-sig-formatter@1.0.11: @@ -14274,6 +17823,8 @@ snapshots: electron-to-chromium@1.5.250: {} + electron-to-chromium@1.5.389: {} + emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} @@ -14302,6 +17853,11 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.0 + enhanced-resolve@5.24.2: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 @@ -14313,12 +17869,18 @@ snapshots: entities@7.0.1: {} + envinfo@7.21.0: {} + environment@1.1.0: {} error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 + error-stack-parser@2.1.4: + dependencies: + stackframe: 1.3.4 + es-abstract@1.24.0: dependencies: array-buffer-byte-length: 1.0.2 @@ -14376,10 +17938,88 @@ snapshots: unbox-primitive: 1.1.0 which-typed-array: 1.1.19 - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - + es-abstract@1.24.2: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.19 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.4.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + math-intrinsics: 1.1.0 + + es-module-lexer@2.3.1: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -14539,6 +18179,10 @@ snapshots: dependencies: eslint: 9.39.1 + eslint-config-prettier@8.8.0(eslint@8.52.0): + dependencies: + eslint: 8.52.0 + eslint-config-xo-space@0.35.0(eslint@9.39.1): dependencies: eslint: 9.39.1 @@ -14638,6 +18282,21 @@ snapshots: - eslint-import-resolver-webpack - supports-color + eslint-plugin-jsdoc@46.8.2(eslint@8.52.0): + dependencies: + '@es-joy/jsdoccomment': 0.40.1 + are-docs-informative: 0.0.2 + comment-parser: 1.4.0 + debug: 4.4.3(supports-color@10.2.2) + escape-string-regexp: 4.0.0 + eslint: 8.52.0 + esquery: 1.6.0 + is-builtin-module: 3.2.1 + semver: 7.7.3 + spdx-expression-parse: 3.0.1 + transitivePeerDependencies: + - supports-color + eslint-plugin-jsdoc@50.8.0(eslint@9.39.1): dependencies: '@es-joy/jsdoccomment': 0.50.2 @@ -14707,6 +18366,30 @@ snapshots: optionalDependencies: eslint-config-prettier: 10.1.8(eslint@9.39.1) + eslint-plugin-react-hooks@4.6.0(eslint@8.52.0): + dependencies: + eslint: 8.52.0 + + eslint-plugin-react@7.33.2(eslint@8.52.0): + dependencies: + array-includes: 3.1.9 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.4.0 + eslint: 8.52.0 + estraverse: 5.3.0 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.5 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.hasown: 1.1.4 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.7 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + eslint-plugin-unicorn@48.0.1(eslint@9.39.1): dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -14746,6 +18429,16 @@ snapshots: semver: 7.7.3 strip-indent: 3.0.0 + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 @@ -14768,6 +18461,49 @@ snapshots: eslint-visitor-keys@4.2.1: {} + eslint@8.52.0: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.52.0) + '@eslint-community/regexpp': 4.12.2 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.52.0 + '@humanwhocodes/config-array': 0.11.14 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@10.2.2) + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.2.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + eslint@9.39.1: dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.1) @@ -14813,6 +18549,12 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.15.0) eslint-visitor-keys: 4.2.1 + espree@9.6.1: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 3.4.3 + esprima@4.0.1: {} esquery@1.6.0: @@ -14823,6 +18565,10 @@ snapshots: dependencies: estraverse: 5.3.0 + esrever@0.2.0: {} + + estraverse@4.3.0: {} + estraverse@5.3.0: {} estree-walker@2.0.2: {} @@ -14833,12 +18579,16 @@ snapshots: eventemitter3@4.0.7: {} + eventemitter3@5.0.1: {} + events-universal@1.0.1: dependencies: bare-events: 2.8.2 transitivePeerDependencies: - bare-abort-controller + events@3.3.0: {} + eventsource-parser@3.0.6: {} eventsource@3.0.7: @@ -14860,6 +18610,8 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.2 + exenv@1.2.2: {} + expand-template@2.0.3: optional: true @@ -15001,6 +18753,8 @@ snapshots: fast-safe-stringify@2.1.1: {} + fast-shallow-equal@1.0.0: {} + fast-uri@3.1.2: {} fast-xml-builder@1.2.0: @@ -15015,8 +18769,12 @@ snapshots: path-expression-matcher: 1.5.0 strnum: 2.3.0 + fast_array_intersect@1.1.0: {} + fastest-levenshtein@1.0.16: {} + fastest-stable-stringify@2.0.2: {} + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -15038,10 +18796,18 @@ snapshots: dependencies: is-unicode-supported: 2.1.0 + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 + file-selector@0.6.0: + dependencies: + tslib: 2.8.1 + filelist@1.0.4: dependencies: minimatch: 5.1.9 @@ -15073,6 +18839,8 @@ snapshots: transitivePeerDependencies: - supports-color + find-root@1.1.0: {} + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -15087,6 +18855,12 @@ snapshots: dependencies: micromatch: 4.0.8 + flat-cache@3.2.0: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + rimraf: 3.0.2 + flat-cache@4.0.1: dependencies: flatted: 3.4.2 @@ -15184,8 +18958,21 @@ snapshots: generator-function@2.0.1: {} + geotiff@2.1.3: + dependencies: + '@petamoriken/float16': 3.9.3 + lerc: 3.0.0 + pako: 2.2.0 + parse-headers: 2.0.6 + quick-lru: 6.1.2 + web-worker: 1.5.0 + xml-utils: 1.10.2 + zstddec: 0.1.0 + get-caller-file@2.0.5: {} + get-document@1.0.0: {} + get-east-asian-width@1.4.0: {} get-func-name@2.0.2: {} @@ -15231,6 +19018,14 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + get-user-locale@2.3.2: + dependencies: + mem: 8.1.1 + + get-window@1.1.2: + dependencies: + get-document: 1.0.0 + git-hooks-list@3.2.0: {} github-from-package@0.0.0: @@ -15340,6 +19135,8 @@ snapshots: graceful-fs@4.2.11: {} + graphemer@1.4.0: {} + graphql@16.12.0: {} has-bigints@1.1.0: {} @@ -15397,8 +19194,23 @@ snapshots: help-me@5.0.0: {} + highlight-words-core@1.2.3: {} + highlight.js@10.7.3: {} + history@4.10.1: + dependencies: + '@babel/runtime': 7.28.4 + loose-envify: 1.4.0 + resolve-pathname: 3.0.0 + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + value-equal: 1.0.1 + + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + hono@4.12.26: {} hookable@5.5.3: {} @@ -15415,6 +19227,10 @@ snapshots: html-escaper@2.0.2: {} + html-parse-stringify@3.0.1: + dependencies: + void-elements: 3.1.0 + html-void-elements@3.0.0: {} htmlparser2@10.1.0: @@ -15495,6 +19311,16 @@ snapshots: human-signals@8.0.1: {} + hyphenate-style-name@1.1.0: {} + + i18next-browser-languagedetector@7.2.2: + dependencies: + '@babel/runtime': 7.28.4 + + i18next@23.16.8: + dependencies: + '@babel/runtime': 7.28.4 + i18next@25.7.4(typescript@5.9.3): dependencies: '@babel/runtime': 7.28.4 @@ -15513,8 +19339,11 @@ snapshots: dependencies: safer-buffer: 2.1.2 - ieee754@1.2.1: - optional: true + icss-utils@5.1.0(postcss@8.5.15): + dependencies: + postcss: 8.5.15 + + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -15522,11 +19351,18 @@ snapshots: immediate@3.0.6: {} + immutable@4.3.5: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + imurmurhash@0.1.4: {} indent-string@4.0.0: {} @@ -15542,14 +19378,33 @@ snapshots: ini@1.3.8: {} + inline-style-prefixer@7.0.1: + dependencies: + css-in-js-utils: 3.1.0 + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 - hasown: 2.0.2 + hasown: 2.0.4 side-channel: 1.1.0 + internmap@2.0.3: {} + interpret@1.4.0: {} + interpret@3.1.1: {} + + intl-messageformat@10.7.18: + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/icu-messageformat-parser': 2.11.4 + tslib: 2.8.1 + + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + ip-address@10.2.0: {} ipaddr.js@1.9.1: {} @@ -15597,6 +19452,10 @@ snapshots: dependencies: hasown: 2.0.2 + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + is-data-view@1.0.2: dependencies: call-bound: 1.0.4 @@ -15632,6 +19491,12 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-hotkey@0.1.4: {} + + is-hotkey@0.2.0: {} + + is-in-browser@1.1.3: {} + is-in-ssh@1.0.0: {} is-inside-container@1.0.0: @@ -15659,6 +19524,10 @@ snapshots: is-plain-obj@4.1.0: {} + is-plain-object@2.0.4: + dependencies: + isobject: 3.0.1 + is-plain-object@5.0.0: {} is-promise@4.0.0: {} @@ -15718,6 +19587,8 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 + is-window@1.0.2: {} + is-windows@1.0.2: {} is-wsl@2.2.0: @@ -15728,6 +19599,8 @@ snapshots: dependencies: is-inside-container: 1.0.0 + isarray@0.0.1: {} + isarray@1.0.0: {} isarray@2.0.5: {} @@ -15736,6 +19609,10 @@ snapshots: isexe@3.1.1: {} + isobject@3.0.1: {} + + isomorphic-base64@1.0.2: {} + istanbul-lib-coverage@3.2.2: {} istanbul-lib-report@3.0.1: @@ -15755,6 +19632,15 @@ snapshots: editions: 6.22.0 textextensions: 6.11.0 + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -15771,10 +19657,20 @@ snapshots: filelist: 1.0.4 picocolors: 1.1.1 + jest-worker@27.5.1: + dependencies: + '@types/node': 20.19.43 + merge-stream: 2.0.0 + supports-color: 8.1.1 + jose@6.1.3: {} joycon@3.1.1: {} + jquery@3.7.1: {} + + js-cookie@2.2.1: {} + js-levenshtein@1.1.6: {} js-tokens@4.0.0: {} @@ -15788,6 +19684,8 @@ snapshots: dependencies: argparse: 2.0.1 + jsdoc-type-pratt-parser@4.0.0: {} + jsdoc-type-pratt-parser@4.1.0: {} jsesc@0.5.0: {} @@ -15808,6 +19706,8 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + json-stringify-pretty-compact@2.0.0: {} + json5@1.0.2: dependencies: minimist: 1.2.8 @@ -15841,6 +19741,13 @@ snapshots: ms: 2.1.3 semver: 7.7.3 + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + jszip@3.10.1: dependencies: lie: 3.3.0 @@ -15871,8 +19778,12 @@ snapshots: dependencies: json-buffer: 3.0.1 + kind-of@6.0.3: {} + kuler@2.0.0: {} + lerc@3.0.0: {} + leven@3.1.0: {} levn@0.4.1: @@ -15892,6 +19803,8 @@ snapshots: dependencies: uc.micro: 2.1.0 + loader-runner@4.3.2: {} + locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -15981,6 +19894,12 @@ snapshots: make-error@1.3.6: {} + map-age-cleaner@0.1.3: + dependencies: + p-defer: 1.0.0 + + mapbox-to-css-font@2.4.5: {} + mark.js@8.11.1: {} markdown-it@14.1.0: @@ -15992,6 +19911,10 @@ snapshots: punycode.js: 2.3.1 uc.micro: 2.1.0 + marked-mangle@1.1.7(marked@12.0.0): + dependencies: + marked: 12.0.0 + marked-terminal@7.3.0(marked@15.0.12): dependencies: ansi-escapes: 7.2.0 @@ -16003,6 +19926,8 @@ snapshots: node-emoji: 2.2.0 supports-hyperlinks: 3.2.0 + marked@12.0.0: {} + marked@15.0.12: {} math-intrinsics@1.1.0: {} @@ -16019,6 +19944,8 @@ snapshots: unist-util-visit: 5.0.0 vfile: 6.0.3 + mdn-data@2.0.14: {} + mdn-data@2.23.0: {} mdurl@2.0.0: {} @@ -16027,14 +19954,29 @@ snapshots: media-typer@1.1.0: {} + mem@8.1.1: + dependencies: + map-age-cleaner: 0.1.3 + mimic-fn: 3.1.0 + + memoize-one@4.0.3: {} + + memoize-one@5.2.1: {} + + memoize-one@6.0.0: {} + merge-descriptors@1.0.3: {} merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} + merge2@1.4.1: {} methods@1.1.2: {} + micro-memoize@4.2.0: {} + micromark-util-character@2.1.1: dependencies: micromark-util-symbol: 2.0.1 @@ -16083,6 +20025,8 @@ snapshots: mime@2.6.0: {} + mimic-fn@3.1.0: {} + mimic-function@5.0.1: {} mimic-response@3.1.0: {} @@ -16091,6 +20035,13 @@ snapshots: min-indent@1.0.1: {} + mini-create-react-context@0.4.1(prop-types@15.8.1)(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + prop-types: 15.8.1 + react: 18.3.1 + tiny-warning: 1.0.3 + minimatch@10.2.4: dependencies: brace-expansion: 5.0.6 @@ -16109,6 +20060,17 @@ snapshots: minimist@1.2.8: {} + minimizer-webpack-plugin@5.6.1(@swc/core@1.15.43(@swc/helpers@0.5.23))(postcss@8.5.15)(webpack@5.108.4): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.49.0 + webpack: 5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(postcss@8.5.15)(webpack-cli@5.1.4) + optionalDependencies: + '@swc/core': 1.15.43(@swc/helpers@0.5.23) + postcss: 8.5.15 + minipass@7.1.2: {} minipass@7.1.3: {} @@ -16153,6 +20115,14 @@ snapshots: yargs-parser: 21.1.1 yargs-unparser: 2.0.0 + moment-timezone@0.5.45: + dependencies: + moment: 2.30.1 + + moment@2.30.1: {} + + monaco-editor@0.34.0: {} + mri@1.2.0: {} ms@2.0.0: {} @@ -16196,6 +20166,19 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 + nano-css@5.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + css-tree: 1.1.3 + csstype: 3.2.3 + fastest-stable-stringify: 2.0.2 + inline-style-prefixer: 7.0.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + rtl-css-js: 1.16.1 + stacktrace-js: 2.0.2 + stylis: 4.4.0 + nanoid@3.3.14: {} napi-build-utils@2.0.0: @@ -16211,6 +20194,8 @@ snapshots: negotiator@1.0.0: {} + neo-async@2.6.2: {} + nise@6.1.5: dependencies: '@sinonjs/commons': 3.0.1 @@ -16240,6 +20225,13 @@ snapshots: emojilib: 2.4.0 skin-tone: 2.0.0 + node-exports-info@1.6.2: + dependencies: + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 @@ -16252,6 +20244,8 @@ snapshots: node-releases@2.0.27: {} + node-releases@2.0.51: {} + node-sarif-builder@3.4.0: dependencies: '@types/sarif': 2.1.7 @@ -16313,6 +20307,13 @@ snapshots: has-symbols: 1.1.0 object-keys: 1.1.1 + object.entries@1.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + object.fromentries@2.0.8: dependencies: call-bind: 1.0.8 @@ -16326,6 +20327,12 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.24.0 + object.hasown@1.1.4: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + object.values@1.2.1: dependencies: call-bind: 1.0.8 @@ -16366,6 +20373,20 @@ snapshots: - aws-crt - supports-color + ol-mapbox-style@10.7.0: + dependencies: + '@mapbox/mapbox-gl-style-spec': 13.28.0 + mapbox-to-css-font: 2.4.5 + ol: 7.4.0 + + ol@7.4.0: + dependencies: + earcut: 2.2.4 + geotiff: 2.1.3 + ol-mapbox-style: 10.7.0 + pbf: 3.2.1 + rbush: 3.0.1 + on-exit-leak-free@2.1.2: {} on-finished@2.4.1: @@ -16457,6 +20478,8 @@ snapshots: p-cancelable@3.0.0: {} + p-defer@1.0.0: {} + p-filter@2.1.0: dependencies: p-map: 2.1.0 @@ -16493,6 +20516,10 @@ snapshots: pako@1.0.11: {} + pako@2.2.0: {} + + papaparse@5.4.1: {} + param-case@3.0.4: dependencies: dot-case: 3.0.4 @@ -16502,6 +20529,8 @@ snapshots: dependencies: callsites: 3.1.0 + parse-headers@2.0.6: {} + parse-imports-exports@0.2.4: dependencies: parse-statements: 1.0.11 @@ -16598,6 +20627,10 @@ snapshots: path-to-regexp@0.1.13: {} + path-to-regexp@1.9.0: + dependencies: + isarray: 0.0.1 + path-to-regexp@3.3.0: {} path-to-regexp@6.3.0: {} @@ -16612,10 +20645,17 @@ snapshots: pathval@1.1.1: {} + pbf@3.2.1: + dependencies: + ieee754: 1.2.1 + resolve-protobuf-schema: 2.1.0 + pend@1.2.0: {} perfect-debounce@2.1.0: {} + performance-now@2.1.0: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -16664,6 +20704,10 @@ snapshots: pkce-challenge@5.0.1: {} + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -16676,6 +20720,34 @@ snapshots: possible-typed-array-names@1.1.0: {} + postcss-modules-extract-imports@3.1.0(postcss@8.5.15): + dependencies: + postcss: 8.5.15 + + postcss-modules-local-by-default@4.2.0(postcss@8.5.15): + dependencies: + icss-utils: 5.1.0(postcss@8.5.15) + postcss: 8.5.15 + postcss-selector-parser: 7.1.4 + postcss-value-parser: 4.2.0 + + postcss-modules-scope@3.2.1(postcss@8.5.15): + dependencies: + postcss: 8.5.15 + postcss-selector-parser: 7.1.4 + + postcss-modules-values@4.0.0(postcss@8.5.15): + dependencies: + icss-utils: 5.1.0(postcss@8.5.15) + postcss: 8.5.15 + + postcss-selector-parser@7.1.4: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + postcss@8.5.15: dependencies: nanoid: 3.3.14 @@ -16700,6 +20772,8 @@ snapshots: tunnel-agent: 0.6.0 optional: true + prefix-style@2.0.1: {} + prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.1: @@ -16714,6 +20788,8 @@ snapshots: dependencies: parse-ms: 4.0.0 + prismjs@1.29.0: {} + prismjs@1.30.0: {} proc-log@4.2.0: {} @@ -16722,6 +20798,12 @@ snapshots: process-warning@5.0.0: {} + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + property-information@7.1.0: {} proto-list@1.2.4: {} @@ -16737,9 +20819,11 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.1 - '@types/node': 22.19.0 + '@types/node': 20.19.43 long: 5.3.2 + protocol-buffers-schema@3.6.1: {} + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -16766,6 +20850,16 @@ snapshots: quick-lru@5.1.1: {} + quick-lru@6.1.2: {} + + quickselect@2.0.0: {} + + raf-schd@4.0.3: {} + + raf@3.4.1: + dependencies: + performance-now: 2.1.0 + rambda@7.5.0: {} range-parser@1.2.0: {} @@ -16786,6 +20880,40 @@ snapshots: iconv-lite: 0.7.2 unpipe: 1.0.0 + rbush@3.0.1: + dependencies: + quickselect: 2.0.0 + + rc-align@2.4.5: + dependencies: + babel-runtime: 6.26.0 + dom-align: 1.12.4 + prop-types: 15.8.1 + rc-util: 4.21.1 + + rc-animate@2.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + babel-runtime: 6.26.0 + classnames: 2.5.1 + css-animation: 1.6.1 + prop-types: 15.8.1 + raf: 3.4.1 + rc-util: 4.21.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-lifecycles-compat: 3.0.4 + + rc-cascader@3.21.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + array-tree-filter: 2.1.0 + classnames: 2.5.1 + rc-select: 14.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-tree: 5.8.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.44.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + rc-config-loader@4.1.3: dependencies: debug: 4.4.3(supports-color@10.2.2) @@ -16795,6 +20923,129 @@ snapshots: transitivePeerDependencies: - supports-color + rc-drawer@6.5.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + '@rc-component/portal': 1.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.44.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-motion@2.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-overflow@1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-resize-observer: 1.4.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.44.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-resize-observer@1.4.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + resize-observer-polyfill: 1.5.1 + + rc-select@14.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + '@rc-component/trigger': 1.18.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-overflow: 1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.44.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-virtual-list: 3.19.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-slider@10.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-time-picker@3.7.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + classnames: 2.5.1 + moment: 2.30.1 + prop-types: 15.8.1 + raf: 3.4.1 + rc-trigger: 2.6.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-lifecycles-compat: 3.0.4 + transitivePeerDependencies: + - react + - react-dom + + rc-tooltip@6.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + '@rc-component/trigger': 1.18.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + classnames: 2.5.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-tree@5.8.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.44.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-virtual-list: 3.19.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + rc-trigger@2.6.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + babel-runtime: 6.26.0 + classnames: 2.5.1 + prop-types: 15.8.1 + rc-align: 2.4.5 + rc-animate: 2.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 4.21.1 + react-lifecycles-compat: 3.0.4 + transitivePeerDependencies: + - react + - react-dom + + rc-util@4.21.1: + dependencies: + add-dom-event-listener: 1.1.0 + prop-types: 15.8.1 + react-is: 16.13.1 + react-lifecycles-compat: 3.0.4 + shallowequal: 1.1.0 + + rc-util@5.44.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-is: 18.3.1 + + rc-virtual-list@3.19.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-resize-observer: 1.4.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-util: 5.44.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -16803,12 +21054,152 @@ snapshots: strip-json-comments: 2.0.1 optional: true + react-aria-components@1.19.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@internationalized/date': 3.12.2 + '@react-types/shared': 3.36.0(react@18.3.1) + '@swc/helpers': 0.5.23 + client-only: 0.0.1 + react: 18.3.1 + react-aria: 3.50.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) + react-stately: 3.48.0(react@18.3.1) + + react-aria@3.50.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@internationalized/date': 3.12.2 + '@internationalized/number': 3.6.7 + '@internationalized/string': 3.2.9 + '@react-types/shared': 3.36.0(react@18.3.1) + '@swc/helpers': 0.5.23 + aria-hidden: 1.2.6 + clsx: 2.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-stately: 3.48.0(react@18.3.1) + use-sync-external-store: 1.6.0(react@18.3.1) + + react-beautiful-dnd@13.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + css-box-model: 1.2.1 + memoize-one: 5.2.1 + raf-schd: 4.0.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-redux: 7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + redux: 4.2.1 + use-memo-one: 1.1.3(react@18.3.1) + transitivePeerDependencies: + - react-native + + react-calendar@4.8.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@wojtekmaj/date-utils': 1.5.1 + clsx: 2.1.1 + get-user-locale: 2.3.2 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + warning: 4.0.3 + optionalDependencies: + '@types/react': 18.3.12 + + react-colorful@5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-custom-scrollbars-2@4.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + dom-css: 2.1.0 + prop-types: 15.8.1 + raf: 3.4.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0 react: 18.3.1 scheduler: 0.23.2 + react-dropzone@14.2.3(react@18.3.1): + dependencies: + attr-accept: 2.2.5 + file-selector: 0.6.0 + prop-types: 15.8.1 + react: 18.3.1 + + react-fast-compare@3.2.2: {} + + react-from-dom@0.6.2(react@18.3.1): + dependencies: + react: 18.3.1 + + react-highlight-words@0.20.0(react@18.3.1): + dependencies: + highlight-words-core: 1.2.3 + memoize-one: 4.0.3 + prop-types: 15.8.1 + react: 18.3.1 + + react-hook-form@7.81.0(react@18.3.1): + dependencies: + react: 18.3.1 + + react-i18next@12.3.1(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + html-parse-stringify: 3.0.1 + i18next: 23.16.8 + react: 18.3.1 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + + react-immutable-proptypes@2.2.0(immutable@4.3.5): + dependencies: + immutable: 4.3.5 + invariant: 2.2.4 + + react-inlinesvg@3.0.2(react@18.3.1): + dependencies: + exenv: 1.2.2 + react: 18.3.1 + react-from-dom: 0.6.2(react@18.3.1) + + react-is@16.13.1: {} + + react-is@17.0.2: {} + + react-is@18.3.1: {} + + react-lifecycles-compat@3.0.4: {} + + react-loading-skeleton@3.4.0(react@18.3.1): + dependencies: + react: 18.3.1 + + react-popper@2.3.0(@popperjs/core@2.11.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@popperjs/core': 2.11.8 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-fast-compare: 3.2.2 + warning: 4.0.3 + + react-redux@7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + '@types/react-redux': 7.1.34 + hoist-non-react-statics: 3.3.2 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-is: 17.0.2 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll-bar@2.3.8(@types/react@18.3.12)(react@18.3.1): dependencies: react: 18.3.1 @@ -16828,11 +21219,63 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 + react-router-dom@5.3.3(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + history: 4.10.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-router: 5.3.3(react@18.3.1) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + react-router@5.3.3(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + history: 4.10.1 + hoist-non-react-statics: 3.3.2 + loose-envify: 1.4.0 + mini-create-react-context: 0.4.1(prop-types@15.8.1)(react@18.3.1) + path-to-regexp: 1.9.0 + prop-types: 15.8.1 + react: 18.3.1 + react-is: 16.13.1 + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + react-select@5.8.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + '@emotion/cache': 11.14.0 + '@emotion/react': 11.11.3(@types/react@18.3.12)(react@18.3.1) + '@floating-ui/dom': 1.7.4 + '@types/react-transition-group': 4.4.12(@types/react@18.3.12) + memoize-one: 6.0.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + use-isomorphic-layout-effect: 1.2.1(@types/react@18.3.12)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - supports-color + react-simple-code-editor@0.14.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + react-stately@3.48.0(react@18.3.1): + dependencies: + '@internationalized/date': 3.12.2 + '@internationalized/number': 3.6.7 + '@internationalized/string': 3.2.9 + '@react-types/shared': 3.36.0(react@18.3.1) + '@swc/helpers': 0.5.23 + react: 18.3.1 + use-sync-external-store: 1.6.0(react@18.3.1) + react-style-singleton@2.2.3(@types/react@18.3.12)(react@18.3.1): dependencies: get-nonce: 1.0.1 @@ -16841,6 +21284,50 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 + react-table@7.8.0(react@18.3.1): + dependencies: + react: 18.3.1 + + react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-universal-interface@0.6.2(react@18.3.1)(tslib@2.8.1): + dependencies: + react: 18.3.1 + tslib: 2.8.1 + + react-use@17.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@types/js-cookie': 2.2.7 + '@xobotyi/scrollbar-width': 1.9.5 + copy-to-clipboard: 3.3.3 + fast-deep-equal: 3.1.3 + fast-shallow-equal: 1.0.0 + js-cookie: 2.2.1 + nano-css: 5.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-universal-interface: 0.6.2(react@18.3.1)(tslib@2.8.1) + resize-observer-polyfill: 1.5.1 + screenfull: 5.2.0 + set-harmonic-interval: 1.0.1 + throttle-debounce: 3.0.1 + ts-easing: 0.2.0 + tslib: 2.8.1 + + react-window@1.8.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + memoize-one: 5.2.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react@18.3.1: dependencies: loose-envify: 1.4.0 @@ -16907,6 +21394,14 @@ snapshots: dependencies: resolve: 1.22.11 + rechoir@0.8.0: + dependencies: + resolve: 1.22.11 + + redux@4.2.1: + dependencies: + '@babel/runtime': 7.28.4 + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -16918,6 +21413,10 @@ snapshots: get-proto: 1.0.1 which-builtin-type: 1.2.1 + regenerator-runtime@0.11.1: {} + + regenerator-runtime@0.14.1: {} + regex-recursion@6.0.2: dependencies: regex-utilities: 2.3.0 @@ -16955,20 +21454,41 @@ snapshots: requires-port@1.0.0: {} + resize-observer-polyfill@1.5.1: {} + resolve-alpn@1.2.1: {} + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + resolve-from@4.0.0: {} resolve-from@5.0.0: {} + resolve-pathname@3.0.0: {} + resolve-pkg-maps@1.0.0: {} + resolve-protobuf-schema@2.1.0: + dependencies: + protocol-buffers-schema: 3.6.1 + resolve@1.22.11: dependencies: is-core-module: 2.16.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + resolve@2.0.0-next.7: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + node-exports-info: 1.6.2 + object-keys: 1.1.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + responselike@3.0.0: dependencies: lowercase-keys: 3.0.0 @@ -16984,6 +21504,12 @@ snapshots: reusify@1.1.0: {} + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + robust-predicates@3.0.3: {} + rollup@4.62.2: dependencies: '@types/estree': 1.0.9 @@ -17025,12 +21551,22 @@ snapshots: transitivePeerDependencies: - supports-color + rtl-css-js@1.16.1: + dependencies: + '@babel/runtime': 7.28.4 + run-applescript@7.1.0: {} run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 + rw@1.3.3: {} + + rxjs@7.8.1: + dependencies: + tslib: 2.8.1 + rxjs@7.8.2: dependencies: tslib: 2.8.1 @@ -17068,6 +21604,15 @@ snapshots: dependencies: loose-envify: 1.4.0 + schema-utils@4.3.3: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.17.1 + ajv-formats: 2.1.1(ajv@8.17.1) + ajv-keywords: 5.1.0(ajv@8.17.1) + + screenfull@5.2.0: {} + secretlint@10.2.2: dependencies: '@secretlint/config-creator': 10.2.2 @@ -17082,6 +21627,8 @@ snapshots: secure-json-parse@4.1.0: {} + selection-is-backward@1.0.0: {} + semver@5.7.2: {} semver@6.3.1: {} @@ -17176,6 +21723,8 @@ snapshots: functions-have-names: 1.2.3 has-property-descriptors: 1.0.2 + set-harmonic-interval@1.0.1: {} + set-proto@1.0.0: dependencies: dunder-proto: 1.0.1 @@ -17186,6 +21735,12 @@ snapshots: setprototypeof@1.2.0: {} + shallow-clone@3.0.1: + dependencies: + kind-of: 6.0.3 + + shallowequal@1.1.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -17281,6 +21836,75 @@ snapshots: slash@5.1.0: {} + slate-base64-serializer@0.2.115(slate@0.47.9(immutable@4.3.5)): + dependencies: + isomorphic-base64: 1.0.2 + slate: 0.47.9(immutable@4.3.5) + + slate-dev-environment@0.2.5: + dependencies: + is-in-browser: 1.1.3 + + slate-hotkeys@0.2.11: + dependencies: + is-hotkey: 0.1.4 + slate-dev-environment: 0.2.5 + + slate-plain-serializer@0.7.13(immutable@4.3.5)(slate@0.47.9(immutable@4.3.5)): + dependencies: + immutable: 4.3.5 + slate: 0.47.9(immutable@4.3.5) + + slate-prop-types@0.5.44(immutable@4.3.5)(slate@0.47.9(immutable@4.3.5)): + dependencies: + immutable: 4.3.5 + slate: 0.47.9(immutable@4.3.5) + + slate-react-placeholder@0.2.9(react@18.3.1)(slate-react@0.22.10(immutable@4.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(slate@0.47.9(immutable@4.3.5)))(slate@0.47.9(immutable@4.3.5)): + dependencies: + react: 18.3.1 + slate: 0.47.9(immutable@4.3.5) + slate-react: 0.22.10(immutable@4.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(slate@0.47.9(immutable@4.3.5)) + + slate-react@0.22.10(immutable@4.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(slate@0.47.9(immutable@4.3.5)): + dependencies: + debug: 3.2.7 + get-window: 1.1.2 + immutable: 4.3.5 + is-window: 1.0.2 + lodash: 4.18.1 + memoize-one: 4.0.3 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-immutable-proptypes: 2.2.0(immutable@4.3.5) + selection-is-backward: 1.0.0 + slate: 0.47.9(immutable@4.3.5) + slate-base64-serializer: 0.2.115(slate@0.47.9(immutable@4.3.5)) + slate-dev-environment: 0.2.5 + slate-hotkeys: 0.2.11 + slate-plain-serializer: 0.7.13(immutable@4.3.5)(slate@0.47.9(immutable@4.3.5)) + slate-prop-types: 0.5.44(immutable@4.3.5)(slate@0.47.9(immutable@4.3.5)) + slate-react-placeholder: 0.2.9(react@18.3.1)(slate-react@0.22.10(immutable@4.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(slate@0.47.9(immutable@4.3.5)))(slate@0.47.9(immutable@4.3.5)) + tiny-invariant: 1.3.3 + tiny-warning: 0.0.3 + transitivePeerDependencies: + - supports-color + + slate@0.47.9(immutable@4.3.5): + dependencies: + debug: 3.2.7 + direction: 0.1.5 + esrever: 0.2.0 + immutable: 4.3.5 + is-plain-object: 2.0.4 + lodash: 4.18.1 + tiny-invariant: 1.3.3 + tiny-warning: 0.0.3 + type-of: 2.0.1 + transitivePeerDependencies: + - supports-color + slice-ansi@4.0.0: dependencies: ansi-styles: 4.3.0 @@ -17296,8 +21920,17 @@ snapshots: dependencies: atomic-sleep: 1.0.0 + sort-asc@0.1.0: {} + + sort-desc@0.1.1: {} + sort-object-keys@1.1.3: {} + sort-object@0.3.2: + dependencies: + sort-asc: 0.1.0 + sort-desc: 0.1.1 + sort-package-json@2.15.1: dependencies: detect-indent: 7.0.2 @@ -17311,6 +21944,17 @@ snapshots: source-map-js@1.2.1: {} + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.5.6: {} + + source-map@0.5.7: {} + + source-map@0.6.1: {} + space-separated-tokens@2.0.2: {} spawn-rx@5.1.2: @@ -17350,8 +21994,27 @@ snapshots: stable-hash@0.0.5: {} + stack-generator@2.0.10: + dependencies: + stackframe: 1.3.4 + stack-trace@0.0.10: {} + stackframe@1.3.4: {} + + stacktrace-gps@3.1.2: + dependencies: + source-map: 0.5.6 + stackframe: 1.3.4 + + stacktrace-js@2.0.2: + dependencies: + error-stack-parser: 2.1.4 + stack-generator: 2.0.10 + stacktrace-gps: 3.1.2 + + state-local@1.0.7: {} + statuses@2.0.1: {} statuses@2.0.2: {} @@ -17374,6 +22037,8 @@ snapshots: strict-event-emitter@0.5.1: {} + string-hash@1.1.3: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -17392,6 +22057,22 @@ snapshots: get-east-asian-width: 1.4.0 strip-ansi: 7.1.2 + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + string.prototype.trim@1.2.10: dependencies: call-bind: 1.0.8 @@ -17457,6 +22138,14 @@ snapshots: dependencies: boundary: 2.0.0 + style-loader@3.3.4(webpack@5.108.4): + dependencies: + webpack: 5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(postcss@8.5.15)(webpack-cli@5.1.4) + + stylis@4.2.0: {} + + stylis@4.4.0: {} + superagent@10.3.0: dependencies: component-emitter: 1.3.1 @@ -17499,10 +22188,20 @@ snapshots: dependencies: '@scarf/scarf': 1.4.0 + swc-loader@0.2.7(@swc/core@1.15.43(@swc/helpers@0.5.23))(webpack@5.108.4): + dependencies: + '@swc/core': 1.15.43(@swc/helpers@0.5.23) + '@swc/counter': 0.1.3 + webpack: 5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(postcss@8.5.15)(webpack-cli@5.1.4) + synckit@0.11.13: dependencies: '@pkgr/core': 0.3.6 + systemjs-cjs-extra@0.2.0: {} + + systemjs@6.14.3: {} + tabbable@6.4.0: {} table@6.9.0: @@ -17519,6 +22218,8 @@ snapshots: tapable@2.3.0: {} + tapable@2.3.3: {} + tar-fs@2.1.4: dependencies: chownr: 1.1.4 @@ -17572,6 +22273,13 @@ snapshots: ansi-escapes: 7.2.0 supports-hyperlinks: 3.2.0 + terser@5.49.0: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.16.0 + commander: 2.20.3 + source-map-support: 0.5.21 + test-exclude@7.0.1: dependencies: '@istanbuljs/schema': 0.1.3 @@ -17610,8 +22318,18 @@ snapshots: dependencies: real-require: 0.2.0 + throttle-debounce@3.0.1: {} + + tiny-invariant@1.3.3: {} + tiny-jsonc@1.0.2: {} + tiny-warning@0.0.3: {} + + tiny-warning@1.0.3: {} + + tinycolor2@1.6.0: {} + tinyexec@1.1.1: {} tinyglobby@0.2.15: @@ -17627,10 +22345,22 @@ snapshots: tmp@0.2.7: {} + to-camel-case@1.0.0: + dependencies: + to-space-case: 1.0.0 + + to-no-case@1.0.2: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 + to-space-case@1.0.0: + dependencies: + to-no-case: 1.0.2 + + toggle-selection@1.0.6: {} + toidentifier@1.0.1: {} tough-cookie@6.0.0: @@ -17645,6 +22375,10 @@ snapshots: triple-beam@1.4.1: {} + ts-api-utils@1.4.3(typescript@5.2.2): + dependencies: + typescript: 5.2.2 + ts-api-utils@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -17654,12 +22388,34 @@ snapshots: picomatch: 4.0.4 typescript: 5.9.3 + ts-easing@0.2.0: {} + ts-morph@27.0.2: dependencies: '@ts-morph/common': 0.28.1 code-block-writer: 13.0.3 - ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3): + ts-node@10.9.2(@swc/core@1.15.43(@swc/helpers@0.5.23))(@types/node@20.19.43)(typescript@5.9.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.19.43 + acorn: 8.16.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 8.0.3 + make-error: 1.3.6 + typescript: 5.9.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.15.43(@swc/helpers@0.5.23) + + ts-node@10.9.2(@swc/core@1.15.43(@swc/helpers@0.5.23))(@types/node@22.19.0)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.12 @@ -17667,15 +22423,17 @@ snapshots: '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 '@types/node': 22.19.0 - acorn: 8.15.0 + acorn: 8.16.0 acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 - diff: 4.0.4 + diff: 8.0.3 make-error: 1.3.6 typescript: 5.9.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.15.43(@swc/helpers@0.5.23) tsconfig-paths@3.15.0: dependencies: @@ -17684,6 +22442,8 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 + tslib@2.6.2: {} + tslib@2.8.1: {} tsx@4.20.6: @@ -17732,6 +22492,8 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.1 + type-of@2.0.1: {} + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -17804,8 +22566,14 @@ snapshots: transitivePeerDependencies: - supports-color + typescript@5.2.2: {} + + typescript@5.3.3: {} + typescript@5.9.3: {} + ua-parser-js@1.0.41: {} + uc.micro@2.1.0: {} ufo@1.6.3: {} @@ -17890,6 +22658,14 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + update-browserslist-db@1.2.3(browserslist@4.28.6): + dependencies: + browserslist: 4.28.6 + escalade: 3.2.0 + picocolors: 1.1.1 + + uplot@1.6.30: {} + upper-case-first@2.0.2: dependencies: tslib: 2.8.1 @@ -17911,6 +22687,16 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 + use-isomorphic-layout-effect@1.2.1(@types/react@18.3.12)(react@18.3.1): + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + use-memo-one@1.1.3(react@18.3.1): + dependencies: + react: 18.3.1 + use-sidecar@1.1.3(@types/react@18.3.12)(react@18.3.1): dependencies: detect-node-es: 1.1.0 @@ -17919,12 +22705,18 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 + use-sync-external-store@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + util-deprecate@1.0.2: {} utils-merge@1.0.1: {} uuid@8.3.2: {} + uuid@9.0.1: {} + v8-compile-cache-lib@3.0.1: {} v8-to-istanbul@9.3.0: @@ -17940,6 +22732,8 @@ snapshots: validate-npm-package-name@5.0.1: {} + value-equal@1.0.1: {} + vary@1.1.2: {} version-range@4.15.0: {} @@ -17954,7 +22748,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite@7.3.5(@types/node@22.19.0)(tsx@4.20.6)(yaml@2.9.0): + vite@7.3.5(@types/node@22.19.0)(terser@5.49.0)(tsx@4.20.6)(yaml@2.9.0): dependencies: esbuild: 0.27.7 fdir: 6.5.0(picomatch@4.0.4) @@ -17965,18 +22759,19 @@ snapshots: optionalDependencies: '@types/node': 22.19.0 fsevents: 2.3.3 + terser: 5.49.0 tsx: 4.20.6 yaml: 2.9.0 - vitepress-plugin-group-icons@1.7.5(vite@7.3.5(@types/node@22.19.0)(tsx@4.20.6)(yaml@2.9.0)): + vitepress-plugin-group-icons@1.7.5(vite@7.3.5(@types/node@22.19.0)(terser@5.49.0)(tsx@4.20.6)(yaml@2.9.0)): dependencies: '@iconify-json/logos': 1.2.11 '@iconify-json/vscode-icons': 1.2.45 '@iconify/utils': 3.1.0 optionalDependencies: - vite: 7.3.5(@types/node@22.19.0)(tsx@4.20.6)(yaml@2.9.0) + vite: 7.3.5(@types/node@22.19.0)(terser@5.49.0)(tsx@4.20.6)(yaml@2.9.0) - vitepress@2.0.0-alpha.17(@types/node@22.19.0)(change-case@5.4.4)(fuse.js@7.1.0)(postcss@8.5.15)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.9.0): + vitepress@2.0.0-alpha.17(@types/node@22.19.0)(change-case@5.4.4)(fuse.js@7.1.0)(postcss@8.5.15)(terser@5.49.0)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.9.0): dependencies: '@docsearch/css': 4.6.2 '@docsearch/js': 4.6.2 @@ -17986,7 +22781,7 @@ snapshots: '@shikijs/transformers': 3.23.0 '@shikijs/types': 3.23.0 '@types/markdown-it': 14.1.2 - '@vitejs/plugin-vue': 6.0.6(vite@7.3.5(@types/node@22.19.0)(tsx@4.20.6)(yaml@2.9.0))(vue@3.5.32(typescript@5.9.3)) + '@vitejs/plugin-vue': 6.0.6(vite@7.3.5(@types/node@22.19.0)(terser@5.49.0)(tsx@4.20.6)(yaml@2.9.0))(vue@3.5.32(typescript@5.9.3)) '@vue/devtools-api': 8.1.1 '@vue/shared': 3.5.32 '@vueuse/core': 14.2.1(vue@3.5.32(typescript@5.9.3)) @@ -17995,7 +22790,7 @@ snapshots: mark.js: 8.11.1 minisearch: 7.2.0 shiki: 3.23.0 - vite: 7.3.5(@types/node@22.19.0)(tsx@4.20.6)(yaml@2.9.0) + vite: 7.3.5(@types/node@22.19.0)(terser@5.49.0)(tsx@4.20.6)(yaml@2.9.0) vue: 3.5.32(typescript@5.9.3) optionalDependencies: postcss: 8.5.15 @@ -18024,6 +22819,8 @@ snapshots: - universal-cookie - yaml + void-elements@3.1.0: {} + vscode-html-languageservice@5.6.0: dependencies: '@vscode/l10n': 0.0.18 @@ -18047,10 +22844,87 @@ snapshots: optionalDependencies: typescript: 5.9.3 + warning@4.0.3: + dependencies: + loose-envify: 1.4.0 + + watchpack@2.5.2: + dependencies: + graceful-fs: 4.2.11 + web-streams-polyfill@3.3.3: {} + web-vitals@4.2.4: {} + + web-worker@1.5.0: {} + webidl-conversions@3.0.1: {} + webpack-cli@5.1.4(webpack@5.108.4): + dependencies: + '@discoveryjs/json-ext': 0.5.7 + '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4)(webpack@5.108.4) + '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4)(webpack@5.108.4) + '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4)(webpack@5.108.4) + colorette: 2.0.20 + commander: 10.0.1 + cross-spawn: 7.0.6 + envinfo: 7.21.0 + fastest-levenshtein: 1.0.16 + import-local: 3.2.0 + interpret: 3.1.1 + rechoir: 0.8.0 + webpack: 5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(postcss@8.5.15)(webpack-cli@5.1.4) + webpack-merge: 5.10.0 + + webpack-merge@5.10.0: + dependencies: + clone-deep: 4.0.1 + flat: 5.0.2 + wildcard: 2.0.1 + + webpack-sources@3.5.1: {} + + webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(postcss@8.5.15)(webpack-cli@5.1.4): + dependencies: + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.16.0 + acorn-import-phases: 1.0.4(acorn@8.16.0) + browserslist: 4.28.6 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.24.2 + es-module-lexer: 2.3.1 + eslint-scope: 5.1.1 + events: 3.3.0 + graceful-fs: 4.2.11 + loader-runner: 4.3.2 + mime-db: 1.54.0 + minimizer-webpack-plugin: 5.6.1(@swc/core@1.15.43(@swc/helpers@0.5.23))(postcss@8.5.15)(webpack@5.108.4) + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.0 + watchpack: 2.5.2 + webpack-sources: 3.5.1 + optionalDependencies: + webpack-cli: 5.1.4(webpack@5.108.4) + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/core' + - '@swc/css' + - '@swc/html' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - uglify-js + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 @@ -18115,6 +22989,8 @@ snapshots: dependencies: string-width: 4.2.3 + wildcard@2.0.1: {} + winston-transport@4.9.0: dependencies: logform: 2.7.0 @@ -18180,6 +23056,8 @@ snapshots: xml-naming@0.1.0: {} + xml-utils@1.10.2: {} + xml2js@0.5.0: dependencies: sax: 1.4.3 @@ -18192,12 +23070,19 @@ snapshots: xmlbuilder@11.0.1: {} + xss@1.0.15: + dependencies: + commander: 2.20.3 + cssfilter: 0.0.10 + y18n@5.0.8: {} yallist@4.0.0: {} yaml-ast-parser@0.0.43: {} + yaml@1.10.3: {} + yaml@2.8.1: {} yaml@2.9.0: {} @@ -18258,4 +23143,6 @@ snapshots: zod@3.25.76: {} + zstddec@0.1.0: {} + zwitch@2.0.4: {}