diff --git a/.github/workflows/cf-webapp-quality-gates.yml b/.github/workflows/cf-webapp-quality-gates.yml new file mode 100644 index 000000000..93294aab6 --- /dev/null +++ b/.github/workflows/cf-webapp-quality-gates.yml @@ -0,0 +1,145 @@ +name: cf-webapp Quality Gates + +on: + pull_request: + paths: + - "js/cf-webapp/**" + +permissions: + contents: read + packages: read + pull-requests: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check-changes: + runs-on: ubuntu-latest + outputs: + should-run: ${{ steps.filter.outputs.webapp }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + webapp: + - 'js/cf-webapp/**' + + skip: + needs: check-changes + if: needs.check-changes.outputs.should-run != 'true' + runs-on: ubuntu-latest + steps: + - run: echo "No cf-webapp changes, skipping." + + benchmark: + needs: check-changes + if: needs.check-changes.outputs.should-run == 'true' + runs-on: ubuntu-latest + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: js/cf-webapp/package-lock.json + registry-url: https://npm.pkg.github.com + scope: "@codeflash-ai" + + - name: Install dependencies + working-directory: js/cf-webapp + run: npm ci --ignore-scripts + + - name: Generate Prisma client + working-directory: js/cf-webapp + run: npx prisma generate + + - name: Type-check + id: typecheck + working-directory: js/cf-webapp + run: npx tsc --noEmit + continue-on-error: true + + - name: Tests + id: tests + working-directory: js/cf-webapp + run: npx vitest run --reporter=verbose 2>&1 | tee test-output.txt + continue-on-error: true + + - name: Build + id: build + working-directory: js/cf-webapp + run: npx next build 2>&1 | tee build-output.txt + continue-on-error: true + + - name: Extract results + id: results + working-directory: js/cf-webapp + run: | + # Type-check status + if [ "${{ steps.typecheck.outcome }}" = "success" ]; then + echo "typecheck_status=✅ Pass" >> "$GITHUB_OUTPUT" + else + echo "typecheck_status=❌ Fail" >> "$GITHUB_OUTPUT" + fi + + # Test summary + if [ "${{ steps.tests.outcome }}" = "success" ]; then + TESTS_SUMMARY=$(grep -E "Tests\s+[0-9]+" test-output.txt | tail -1 || echo "passed") + echo "tests_status=✅ ${TESTS_SUMMARY}" >> "$GITHUB_OUTPUT" + else + echo "tests_status=❌ Tests failed" >> "$GITHUB_OUTPUT" + fi + + # Build status + if [ "${{ steps.build.outcome }}" = "success" ]; then + echo "build_status=✅ Success" >> "$GITHUB_OUTPUT" + else + echo "build_status=❌ Fail" >> "$GITHUB_OUTPUT" + fi + + # Extract route sizes from build output + ROUTES=$(sed -n '/Route.*Size.*First Load/,/^$/p' build-output.txt | head -30 || echo "No route data") + { + echo "routes<> "$GITHUB_OUTPUT" + + - name: Post PR comment + if: github.event_name == 'pull_request' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr comment ${{ github.event.pull_request.number }} \ + --repo ${{ github.repository }} \ + --body "$(cat <<'COMMENT_EOF' + ## cf-webapp Quality Report + + | Check | Result | + |-------|--------| + | Type-check | ${{ steps.results.outputs.typecheck_status }} | + | Tests | ${{ steps.results.outputs.tests_status }} | + | Build | ${{ steps.results.outputs.build_status }} | + +
+ Route Sizes + + ``` + ${{ steps.results.outputs.routes }} + ``` +
+ COMMENT_EOF + )" + + - name: Fail if any check failed + if: steps.typecheck.outcome == 'failure' || steps.tests.outcome == 'failure' || steps.build.outcome == 'failure' + run: exit 1 diff --git a/django/aiservice/core/languages/js_ts/testgen.py b/django/aiservice/core/languages/js_ts/testgen.py index d3a0b6232..c5d543b4e 100644 --- a/django/aiservice/core/languages/js_ts/testgen.py +++ b/django/aiservice/core/languages/js_ts/testgen.py @@ -605,7 +605,10 @@ async def testgen_javascript( ) # Strip incorrect file extensions from import paths (LLMs sometimes add .js to .ts imports) + # Must strip from ALL three test outputs since CLI uses instrumented versions generated_test_source = strip_js_extensions(generated_test_source) + instrumented_behavior_tests = strip_js_extensions(instrumented_behavior_tests) + instrumented_perf_tests = strip_js_extensions(instrumented_perf_tests) ph(request.user, "aiservice-testgen-tests-generated", properties={"language": language}) diff --git a/django/aiservice/tests/testgen/test_testgen_javascript.py b/django/aiservice/tests/testgen/test_testgen_javascript.py index ce3c406b3..baed5683a 100644 --- a/django/aiservice/tests/testgen/test_testgen_javascript.py +++ b/django/aiservice/tests/testgen/test_testgen_javascript.py @@ -433,3 +433,41 @@ import { resolveCredentialsDir } from '../config/paths.js';""" # No .js should remain assert ".js" not in result + + +class TestInstrumentedTestsExtensionStripping: + """Tests for ensuring .js extensions are stripped from ALL test outputs.""" + + def test_strip_extensions_on_all_outputs(self) -> None: + """Test that .js extensions should be stripped from instrumented tests too. + + This is a regression test for the bug where strip_js_extensions() was only + called on generated_test_source but not on instrumented_behavior_tests + and instrumented_perf_tests, causing "Cannot find module" errors in the CLI. + """ + # Simulated LLM output with .js extensions (what comes back from LLM) + llm_generated_test = """import { buildVerifyFn } from '../../google.js'; +import { authenticate } from '../../sso.js'; + +test('should create verify function', () => { + const fn = buildVerifyFn(mockSave); + expect(fn).toBeDefined(); +});""" + + # All three test outputs should have extensions stripped + # (in practice, instrumented tests have capture() calls added, but for this test we're checking extension stripping) + expected_stripped = """import { buildVerifyFn } from '../../google'; +import { authenticate } from '../../sso'; + +test('should create verify function', () => { + const fn = buildVerifyFn(mockSave); + expect(fn).toBeDefined(); +});""" + + # Verify that strip_js_extensions works + result = strip_js_extensions(llm_generated_test) + assert result == expected_stripped, "strip_js_extensions should remove .js extensions" + + # Regression test: verifies strip_js_extensions() is applied correctly. + # For full end-to-end coverage, an integration test calling testgen_javascript() + # and asserting all three return values would be ideal. diff --git a/js/cf-webapp/.playwright-mcp/page-2026-04-03T19-14-17-439Z.yml b/js/cf-webapp/.playwright-mcp/page-2026-04-03T19-14-17-439Z.yml new file mode 100644 index 000000000..2ebc57846 --- /dev/null +++ b/js/cf-webapp/.playwright-mcp/page-2026-04-03T19-14-17-439Z.yml @@ -0,0 +1,34 @@ +- generic [ref=e2]: + - generic [ref=e4]: + - img [ref=e6] + - generic [ref=e12]: + - heading "Get started with Codeflash" [level=1] [ref=e13] + - paragraph [ref=e14]: Make all your code optimal + - button "Continue with GitHub" [ref=e15] [cursor=pointer]: + - img [ref=e16] + - generic [ref=e18]: Continue with GitHub + - generic [ref=e20]: + - link "Terms" [ref=e21] [cursor=pointer]: + - /url: https://www.codeflash.ai/terms-of-service + - link "Privacy" [ref=e22] [cursor=pointer]: + - /url: https://www.codeflash.ai/privacy-policy + - link "Documentation" [ref=e23] [cursor=pointer]: + - /url: https://docs.codeflash.ai + - generic [ref=e25]: + - heading "Always Ship Optimal Code" [level=2] [ref=e27] + - generic [ref=e28]: + - generic [ref=e29]: + - img [ref=e31] + - paragraph [ref=e34]: VS Code/Cursor Extension to optimize all code locally + - generic [ref=e35]: + - img [ref=e37] + - paragraph [ref=e40]: Set it as a GitHub action to automate optimization + - generic [ref=e41]: + - img [ref=e43] + - paragraph [ref=e46]: Codeflash finds 2-55x performance improvements automatically + - generic [ref=e47]: + - img [ref=e49] + - paragraph [ref=e52]: Confidently merge the tested and proven optimizations + - generic [ref=e53]: + - img [ref=e55] + - paragraph [ref=e58]: Start free. No credit card, no lock-in \ No newline at end of file diff --git a/js/cf-webapp/next.config.mjs b/js/cf-webapp/next.config.mjs index fa52bd91b..81a49ee79 100644 --- a/js/cf-webapp/next.config.mjs +++ b/js/cf-webapp/next.config.mjs @@ -1,6 +1,11 @@ +import bundleAnalyzer from "@next/bundle-analyzer" import { dirname } from "path" import { fileURLToPath } from "url" +const withBundleAnalyzer = bundleAnalyzer({ + enabled: process.env.ANALYZE === "true", +}) + const __dirname = dirname(fileURLToPath(import.meta.url)) /** @type {import("next").NextConfig} */ @@ -71,7 +76,7 @@ const nextConfig = { import { withSentryConfig } from "@sentry/nextjs" -export default withSentryConfig( +export default withBundleAnalyzer(withSentryConfig( nextConfig, { // For all available options, see: @@ -101,4 +106,4 @@ export default withSentryConfig( // Disable automatic instrumentation that might cause issues automaticVercelMonitors: false, }, -) +)) diff --git a/js/cf-webapp/package-lock.json b/js/cf-webapp/package-lock.json index 6acfa459c..11bd87fc0 100644 --- a/js/cf-webapp/package-lock.json +++ b/js/cf-webapp/package-lock.json @@ -11,11 +11,13 @@ "dependencies": { "@anthropic-ai/sdk": "^0.74.0", "@auth0/nextjs-auth0": "^4", - "@azure/msal-node": "^3.7.3", "@codeflash-ai/common": "^1.0.30", "@hookform/resolvers": "^3.3.2", "@monaco-editor/react": "^4.7.0", + "@opentelemetry/auto-instrumentations-node": "^0.72.0", + "@opentelemetry/sdk-node": "^0.214.0", "@prisma/client": "^6.7.0", + "@prisma/instrumentation": "^7.6.0", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", @@ -28,25 +30,27 @@ "@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-tooltip": "^1.1.4", "@sentry/nextjs": "^10.38.0", + "@sentry/opentelemetry": "^10.47.0", "@types/node": "^24.3.0", "@types/pg": "^8.10.9", "@types/react": "19.2.13", "@types/react-dom": "19.2.3", "@types/react-syntax-highlighter": "^15.5.13", "chart.js": "^4.4.9", + "chartjs-plugin-datalabels": "^2.2.0", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "date-fns": "^4.1.0", "diff": "^8.0.2", - "framer-motion": "^12.12.1", - "github-markdown-css": "^5.4.0", "jsonwebtoken": "^9.0.2", "lucide-react": "^0.563.0", "marked": "^16.1.1", + "motion": "^12.38.0", "next": "16.1.6", "next-themes": "^0.4.6", "node-ts-cache": "^4.4.0", "node-ts-cache-storage-memory": "^4.4.0", + "papaparse": "^5.5.3", "pg": "^8.11.3", "postcss": "^8", "posthog-js": "1.127.0", @@ -57,7 +61,6 @@ "react-dom": "19.2.4", "react-hook-form": "^7.48.2", "react-markdown": "^9.0.1", - "react-papaparse": "^4.4.0", "react-resizable-panels": "^4.6.4", "react-syntax-highlighter": "^16.1.0", "remark-gfm": "^4.0.0", @@ -70,8 +73,10 @@ "zod": "^3.22.4" }, "devDependencies": { + "@next/bundle-analyzer": "^16.2.2", "@testing-library/react": "^16.0.0", "@types/jsonwebtoken": "^9.0.10", + "@types/papaparse": "^5.5.2", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.0.1", "baseline-browser-mapping": "^2.9.11", @@ -865,6 +870,16 @@ "node": ">=18" } }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@edge-runtime/cookies": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@edge-runtime/cookies/-/cookies-5.0.2.tgz", @@ -1681,6 +1696,37 @@ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@hookform/resolvers": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", @@ -2315,6 +2361,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@kurkle/color": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", @@ -2357,6 +2413,16 @@ "@tybys/wasm-util": "^0.10.0" } }, + "node_modules/@next/bundle-analyzer": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-16.2.2.tgz", + "integrity": "sha512-wIpZBHyCv1y+y0dnFmqQFlpX91V94Z7RKnpu1TRP0BRaWB/1M74Iqxa9IJ8lzSO0FyTqKldgDHoQJJmn8F+gIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "webpack-bundle-analyzer": "4.10.1" + } + }, "node_modules/@next/env": { "version": "16.1.6", "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz", @@ -2579,6 +2645,85 @@ "node": ">=8.0.0" } }, + "node_modules/@opentelemetry/auto-instrumentations-node": { + "version": "0.72.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.72.0.tgz", + "integrity": "sha512-OmzmCENHbvnbt6U+dIj4v75FL6lV+b10Id70AL++iuGTrOeqpDyh04t51KeHN70NEHvzl+kEglcDlZqgmL0LLA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/instrumentation-amqplib": "^0.61.0", + "@opentelemetry/instrumentation-aws-lambda": "^0.66.0", + "@opentelemetry/instrumentation-aws-sdk": "^0.69.0", + "@opentelemetry/instrumentation-bunyan": "^0.59.0", + "@opentelemetry/instrumentation-cassandra-driver": "^0.59.0", + "@opentelemetry/instrumentation-connect": "^0.57.0", + "@opentelemetry/instrumentation-cucumber": "^0.30.0", + "@opentelemetry/instrumentation-dataloader": "^0.31.0", + "@opentelemetry/instrumentation-dns": "^0.57.0", + "@opentelemetry/instrumentation-express": "^0.62.0", + "@opentelemetry/instrumentation-fs": "^0.33.0", + "@opentelemetry/instrumentation-generic-pool": "^0.57.0", + "@opentelemetry/instrumentation-graphql": "^0.62.0", + "@opentelemetry/instrumentation-grpc": "^0.214.0", + "@opentelemetry/instrumentation-hapi": "^0.60.0", + "@opentelemetry/instrumentation-http": "^0.214.0", + "@opentelemetry/instrumentation-ioredis": "^0.62.0", + "@opentelemetry/instrumentation-kafkajs": "^0.23.0", + "@opentelemetry/instrumentation-knex": "^0.58.0", + "@opentelemetry/instrumentation-koa": "^0.62.0", + "@opentelemetry/instrumentation-lru-memoizer": "^0.58.0", + "@opentelemetry/instrumentation-memcached": "^0.57.0", + "@opentelemetry/instrumentation-mongodb": "^0.67.0", + "@opentelemetry/instrumentation-mongoose": "^0.60.0", + "@opentelemetry/instrumentation-mysql": "^0.60.0", + "@opentelemetry/instrumentation-mysql2": "^0.60.0", + "@opentelemetry/instrumentation-nestjs-core": "^0.60.0", + "@opentelemetry/instrumentation-net": "^0.58.0", + "@opentelemetry/instrumentation-openai": "^0.12.0", + "@opentelemetry/instrumentation-oracledb": "^0.39.0", + "@opentelemetry/instrumentation-pg": "^0.66.0", + "@opentelemetry/instrumentation-pino": "^0.60.0", + "@opentelemetry/instrumentation-redis": "^0.62.0", + "@opentelemetry/instrumentation-restify": "^0.59.0", + "@opentelemetry/instrumentation-router": "^0.58.0", + "@opentelemetry/instrumentation-runtime-node": "^0.27.0", + "@opentelemetry/instrumentation-socket.io": "^0.61.0", + "@opentelemetry/instrumentation-tedious": "^0.33.0", + "@opentelemetry/instrumentation-undici": "^0.24.0", + "@opentelemetry/instrumentation-winston": "^0.58.0", + "@opentelemetry/resource-detector-alibaba-cloud": "^0.33.4", + "@opentelemetry/resource-detector-aws": "^2.14.0", + "@opentelemetry/resource-detector-azure": "^0.22.0", + "@opentelemetry/resource-detector-container": "^0.8.5", + "@opentelemetry/resource-detector-gcp": "^0.49.0", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/sdk-node": "^0.214.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/core": "^2.0.0" + } + }, + "node_modules/@opentelemetry/configuration": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/configuration/-/configuration-0.214.0.tgz", + "integrity": "sha512-Q+awuEwxhETwIAXuxHvIY5ZMEP0ZqvxLTi9kclrkyVJppEUXYL3Bhiw3jYrxdHYMh0Y0tVInQH9FEZ1aMinvLA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "yaml": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + } + }, "node_modules/@opentelemetry/context-async-hooks": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.6.1.tgz", @@ -2606,6 +2751,222 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.214.0.tgz", + "integrity": "sha512-SwmFRwO8mi6nndzbsjPgSFg7qy1WeNHRFD+s6uCsdiUDUt3+yzI2qiHE3/ub2f37+/CbeGcG+Ugc8Gwr6nu2Aw==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.14.3", + "@opentelemetry/core": "2.6.1", + "@opentelemetry/otlp-exporter-base": "0.214.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.214.0", + "@opentelemetry/otlp-transformer": "0.214.0", + "@opentelemetry/sdk-logs": "0.214.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.214.0.tgz", + "integrity": "sha512-9qv2Tl/Hq6qc5pJCbzFJnzA0uvlb9DgM70yGJPYf3bA5LlLkRCpcn81i4JbcIH4grlQIWY6A+W7YG0LLvS1BAw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.214.0", + "@opentelemetry/core": "2.6.1", + "@opentelemetry/otlp-exporter-base": "0.214.0", + "@opentelemetry/otlp-transformer": "0.214.0", + "@opentelemetry/sdk-logs": "0.214.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.214.0.tgz", + "integrity": "sha512-IWAVvCO1TlpotRjFmhQFz9RSfQy5BsLtDRBtptSrXZRwfyRPpuql/RMe5zdmu0Gxl3ERDFwOzOqkf3bwy7Jzcw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.214.0", + "@opentelemetry/core": "2.6.1", + "@opentelemetry/otlp-exporter-base": "0.214.0", + "@opentelemetry/otlp-transformer": "0.214.0", + "@opentelemetry/resources": "2.6.1", + "@opentelemetry/sdk-logs": "0.214.0", + "@opentelemetry/sdk-trace-base": "2.6.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.214.0.tgz", + "integrity": "sha512-0NGxWHVYHgbp51SEzmsP+Hdups81eRs229STcSWHo3WO0aqY6RpJ9csxfyEtFgaNrBDv6UfOh0je4ss/ROS6XA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.14.3", + "@opentelemetry/core": "2.6.1", + "@opentelemetry/exporter-metrics-otlp-http": "0.214.0", + "@opentelemetry/otlp-exporter-base": "0.214.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.214.0", + "@opentelemetry/otlp-transformer": "0.214.0", + "@opentelemetry/resources": "2.6.1", + "@opentelemetry/sdk-metrics": "2.6.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.214.0.tgz", + "integrity": "sha512-Tx/59RmjBgkXJ3qnsD04rpDrVWL53LU/czpgLJh+Ab98nAroe91I7vZ3uGN9mxwPS0jsZEnmqmHygVwB2vRMlA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/otlp-exporter-base": "0.214.0", + "@opentelemetry/otlp-transformer": "0.214.0", + "@opentelemetry/resources": "2.6.1", + "@opentelemetry/sdk-metrics": "2.6.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.214.0.tgz", + "integrity": "sha512-pJIcghFGhx3VSCgP5U+yZx+OMNj0t+ttnhC8IjL5Wza7vWIczctF6t3AGcVQffi2dEqX+ZHANoBwoPR8y6RMKA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/exporter-metrics-otlp-http": "0.214.0", + "@opentelemetry/otlp-exporter-base": "0.214.0", + "@opentelemetry/otlp-transformer": "0.214.0", + "@opentelemetry/resources": "2.6.1", + "@opentelemetry/sdk-metrics": "2.6.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.214.0.tgz", + "integrity": "sha512-4TGYoZKebUWVuYkV6r5wS2dUF4zH7EbWFw/Uqz1ZM1tGHQeFT9wzHGXq3iSIXMUrwu5jRdxjfMaXrYejPu2kpQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/resources": "2.6.1", + "@opentelemetry/sdk-metrics": "2.6.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.214.0.tgz", + "integrity": "sha512-FWRZ7AWoTryYhthralHkfXUuyO3l7cRsnr49WcDio1orl2a7KxT8aDZdwQtV1adzoUvZ9Gfo+IstElghCS4zfw==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.14.3", + "@opentelemetry/core": "2.6.1", + "@opentelemetry/otlp-exporter-base": "0.214.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.214.0", + "@opentelemetry/otlp-transformer": "0.214.0", + "@opentelemetry/resources": "2.6.1", + "@opentelemetry/sdk-trace-base": "2.6.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.214.0.tgz", + "integrity": "sha512-kIN8nTBMgV2hXzV/a20BCFilPZdAIMYYJGSgfMMRm/Xa+07y5hRDS2Vm12A/z8Cdu3Sq++ZvJfElokX2rkgGgw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/otlp-exporter-base": "0.214.0", + "@opentelemetry/otlp-transformer": "0.214.0", + "@opentelemetry/resources": "2.6.1", + "@opentelemetry/sdk-trace-base": "2.6.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.214.0.tgz", + "integrity": "sha512-ON0spYWb2yAdQ9b+ItNyK0c6qdtcs+0eVR4YFJkhJL7agfT8sHFg0e5EesauSRiTHPZHiDobI92k77q0lwAmqg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/otlp-exporter-base": "0.214.0", + "@opentelemetry/otlp-transformer": "0.214.0", + "@opentelemetry/resources": "2.6.1", + "@opentelemetry/sdk-trace-base": "2.6.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.6.1.tgz", + "integrity": "sha512-km2/hD3inLTqtLnUAHDGz7ZP/VOyZNslrC/iN66x4jkmpckwlONW54LRPNI6fm09/musDtZga9EWsxgwnjGUlw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/resources": "2.6.1", + "@opentelemetry/sdk-trace-base": "2.6.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, "node_modules/@opentelemetry/instrumentation": { "version": "0.214.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.214.0.tgz", @@ -2640,6 +3001,73 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-aws-lambda": { + "version": "0.66.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-lambda/-/instrumentation-aws-lambda-0.66.0.tgz", + "integrity": "sha512-ObWWLwgjMXTsvete1O78ULLEKur9GdFLR+TvGGb56Srih7ifwcWa2jsnq+4PI8k5wwHuEyxB5SlMjwkKW7rTCQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/aws-lambda": "^8.10.155" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-aws-sdk": { + "version": "0.69.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.69.0.tgz", + "integrity": "sha512-JfSp3anFL5Lx/ysQSa4MnKxvSsXSnYpgQ831Y+yNs5wJZcJC4tB+YpnKH+bU5oFdKEF59FpI6Gn5Wg2vjVpR2A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.34.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-bunyan": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-bunyan/-/instrumentation-bunyan-0.59.0.tgz", + "integrity": "sha512-XaZoIpc2U/WxE//kEyQsGuke9JezPOeeWJUkbHkZt+ojzPbYcAXZR4m9KmxSNbHu++bx1Zy3oBQ3erEXHGoDqA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.214.0", + "@opentelemetry/instrumentation": "^0.214.0", + "@types/bunyan": "1.8.11" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-cassandra-driver": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cassandra-driver/-/instrumentation-cassandra-driver-0.59.0.tgz", + "integrity": "sha512-WtbENFKo4HRBwyffUEN+LSTdjDrBMyfaEYO362VVEhLoFWsFbGGXVApL7rIOhM2LjL04Oel6uJyJC6E4nvCgAA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.37.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/instrumentation-connect": { "version": "0.57.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.57.0.tgz", @@ -2658,6 +3086,22 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-cucumber": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cucumber/-/instrumentation-cucumber-0.30.0.tgz", + "integrity": "sha512-Zx/PXw5o6VkMRcDT+SizbBTJiWdnkivsrVeFgaT1KM14bSbBULPNms+NX6/gsgD0Mkfik3np7HjfKyvipwQ9FA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, "node_modules/@opentelemetry/instrumentation-dataloader": { "version": "0.31.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.31.0.tgz", @@ -2673,6 +3117,21 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-dns": { + "version": "0.57.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dns/-/instrumentation-dns-0.57.0.tgz", + "integrity": "sha512-VJ0p1y0lPhDTIT/kuSgZOG2FJceFQfFgjKCz6k0rh+MyZKwEDTqvmkZUbA8qwgWB5m3fMqttK73jWZyzQNZnTw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.214.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/instrumentation-express": { "version": "0.62.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.62.0.tgz", @@ -2736,6 +3195,22 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-grpc": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-grpc/-/instrumentation-grpc-0.214.0.tgz", + "integrity": "sha512-qU7NMLuXvu+ZvX6LJWJuxfqHvUvCAexduBWnM7OFUVHnkwo/HorWa9qyDFBXEdUE2fypCcYWZkon37wv9y/lDw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "0.214.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/instrumentation-hapi": { "version": "0.60.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.60.0.tgz", @@ -2852,6 +3327,23 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-memcached": { + "version": "0.57.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-memcached/-/instrumentation-memcached-0.57.0.tgz", + "integrity": "sha512-z/a4vC+hmQn4o+NYgDlQE5DJNKH9nwtzvTOAgG1bwO1hdX+w9Nr3kd9dKRwN7e6EiQESrPCh6iiE0xwh9x1WDw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "@types/memcached": "^2.2.6" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/instrumentation-mongodb": { "version": "0.67.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.67.0.tgz", @@ -2919,6 +3411,72 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-nestjs-core": { + "version": "0.60.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.60.0.tgz", + "integrity": "sha512-BZqFAoD+frnwjpb0/T4kEEQMhl2YykZch4n2MMLKAVTzTehTBBV2hZxvFF629ipS+WOGBKjCjz1dycU9QNIckQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.30.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-net": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-net/-/instrumentation-net-0.58.0.tgz", + "integrity": "sha512-NkvEqgt8etd4dwJ+KlKMBzf7SQd+TVVu5UlB1Rt8aOabZ7X3QWCnkgRzfXozAMkZJmUQ3KV4NsBI5nvmngNUdA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.33.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-openai": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-openai/-/instrumentation-openai-0.12.0.tgz", + "integrity": "sha512-HPEw6Zgk/6oMgO/azb7TuYziaU87FnaFTpd74MXqPk2YUhCcRFwT3YZywO/VQ0sjhDX/TqTPEMemTEPwuQNU4w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.214.0", + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.36.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-oracledb": { + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-oracledb/-/instrumentation-oracledb-0.39.0.tgz", + "integrity": "sha512-CmRiX9Khbui9CQS3ZOOmf8RfXdmwSdVJAWQUk8S/gQqlm7xwK853rsP5T1GBSqGyntM9c2En3KpgRGvmk+LCvg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.34.0", + "@types/oracledb": "6.5.2" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/instrumentation-pg": { "version": "0.66.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.66.0.tgz", @@ -2939,6 +3497,23 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-pino": { + "version": "0.60.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pino/-/instrumentation-pino-0.60.0.tgz", + "integrity": "sha512-B36CgHiloKjkFlXkyh3qb4E/KNdnQiO6q8NqKBjYayvvZodshnvz5kPyaV+Fk0N30NwOHn/JgmO1x5tcjYtUvQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.214.0", + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.214.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/instrumentation-redis": { "version": "0.62.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.62.0.tgz", @@ -2956,6 +3531,69 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-restify": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-restify/-/instrumentation-restify-0.59.0.tgz", + "integrity": "sha512-zQ8M7acaHR3STolma45wLqleYJdRMs+cuVtyVgHSBZusyv6FTDxQs8sGVfvitmxThUATo/xlbXSUEwEO/itgLg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-router": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-router/-/instrumentation-router-0.58.0.tgz", + "integrity": "sha512-0txTRUeQn+nDofZ0hQ1i4DuNURA7DnewfxcdmwfA0LMFNY1DZsr47vm6yfEezkii3eIGW+lubipjPYawxXYwzw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-runtime-node": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-runtime-node/-/instrumentation-runtime-node-0.27.0.tgz", + "integrity": "sha512-5S/Xd03scYSSZX3Pg6qfxIgpq2CCUIqBoJPnIgE41NM1tLiCm9zplQw6+699Uhj97mIthBHsGTwgdJCBc1vzkg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.214.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-socket.io": { + "version": "0.61.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-socket.io/-/instrumentation-socket.io-0.61.0.tgz", + "integrity": "sha512-/yhFfR/iW8nf+sgHn5KLiPauF//rTP7a/Hxcl/khgXzbVPsT1AhRvJ8HbPvNVWrJqki52ztucuEFeO00DcncyQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.214.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/instrumentation-tedious": { "version": "0.33.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.33.0.tgz", @@ -2990,6 +3628,107 @@ "@opentelemetry/api": "^1.7.0" } }, + "node_modules/@opentelemetry/instrumentation-winston": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-winston/-/instrumentation-winston-0.58.0.tgz", + "integrity": "sha512-v64eFPrWG7u2xZzU/Zz/jbMIL4etoLrqGqeLyVIW2rxwzp2QriGZEk90Xt2p7Yo/WBbTnl5nuruIinhNG406IA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.214.0", + "@opentelemetry/instrumentation": "^0.214.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.214.0.tgz", + "integrity": "sha512-u1Gdv0/E9wP+apqWf7Wv2npXmgJtxsW2XL0TEv9FZloTZRuMBKmu8cYVXwS4Hm3q/f/3FuCnPTgiwYvIqRSpRg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/otlp-transformer": "0.214.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.214.0.tgz", + "integrity": "sha512-IDP6zcyA24RhNZ289MP6eToIZcinlmirHjX8v3zKCQ2ZhPpt5cGwkN91tCth337lqHIgWcTy90uKRiX/SzALDw==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.14.3", + "@opentelemetry/core": "2.6.1", + "@opentelemetry/otlp-exporter-base": "0.214.0", + "@opentelemetry/otlp-transformer": "0.214.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.214.0.tgz", + "integrity": "sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.214.0", + "@opentelemetry/core": "2.6.1", + "@opentelemetry/resources": "2.6.1", + "@opentelemetry/sdk-logs": "0.214.0", + "@opentelemetry/sdk-metrics": "2.6.1", + "@opentelemetry/sdk-trace-base": "2.6.1", + "protobufjs": "^7.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/propagator-b3": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.6.1.tgz", + "integrity": "sha512-Dvz9TA6cPqIbxolSzQ5x9br6iQlqdGhVYrm+oYc7pfJ7LaVXz8F0XIqhWbnKB5YvfZ6SUmabBUUxnvHs/9uhxA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-jaeger": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.6.1.tgz", + "integrity": "sha512-kKFMxBcjBZAC1vBch5mtZ/dJQvcAEKWga+c+q5iGgRLPIE6Mc649zEwMaCIQCzalziMJQiyUadFYMHmELB7AFw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, "node_modules/@opentelemetry/redis-common": { "version": "0.38.2", "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.38.2.tgz", @@ -2999,6 +3738,89 @@ "node": "^18.19.0 || >=20.6.0" } }, + "node_modules/@opentelemetry/resource-detector-alibaba-cloud": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-alibaba-cloud/-/resource-detector-alibaba-cloud-0.33.4.tgz", + "integrity": "sha512-S07KBOB3+BHV0xjuN4sCRP7x44p2rW0ieGDzoRu1f8Sbvw9Gw4f1oL83tfXiOb0fGPVt8DF4P+39UcggHQsACA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/resource-detector-aws": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-aws/-/resource-detector-aws-2.14.0.tgz", + "integrity": "sha512-1a0YMG6wZuLUfwkSgfe77vN60V5SmK//kM+JsQFT7dOKLyFvpN5A+TpX/eFdaqnhg89CxyF7XpKMBbg1DGv5bw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/resource-detector-azure": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-azure/-/resource-detector-azure-0.22.0.tgz", + "integrity": "sha512-/cYJBFACVqPSWNFU2gdx/wh8kB98YK4dyIhWh1IU2z1iFDrLHpwVjEIS8xLazSqJDntTTqeb8GVSlUlPF3B1pg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/semantic-conventions": "^1.37.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/resource-detector-container": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-container/-/resource-detector-container-0.8.5.tgz", + "integrity": "sha512-vWlfpiCHKWVrT/3EHgJfRLGX8ghVsEZ6CBHhJo5sAQQnwInDNcXjbBJm74Jiyqt0eg7NLeT0EfpXHCUSeYgFaA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/resource-detector-gcp": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-gcp/-/resource-detector-gcp-0.49.0.tgz", + "integrity": "sha512-JP4wrArxUBEGUCfd4SijKJXjspVs/3/eGH6siIlaVdRwf0XLEi4lXI+MdQuWSo4L4sEUCj6igojYzsuHZiuWDA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0", + "gcp-metadata": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, "node_modules/@opentelemetry/resources": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", @@ -3015,6 +3837,79 @@ "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.214.0.tgz", + "integrity": "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.214.0", + "@opentelemetry/core": "2.6.1", + "@opentelemetry/resources": "2.6.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.6.1.tgz", + "integrity": "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/resources": "2.6.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.214.0.tgz", + "integrity": "sha512-gl2XvQBJuPjhGcw9SsnQO5qxChAPMuGRPFaD8lqtF+Cey91NgGUQ0sio2vlDFOSm3JOLzc44vL+OAfx1dXuZjg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.214.0", + "@opentelemetry/configuration": "0.214.0", + "@opentelemetry/context-async-hooks": "2.6.1", + "@opentelemetry/core": "2.6.1", + "@opentelemetry/exporter-logs-otlp-grpc": "0.214.0", + "@opentelemetry/exporter-logs-otlp-http": "0.214.0", + "@opentelemetry/exporter-logs-otlp-proto": "0.214.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "0.214.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.214.0", + "@opentelemetry/exporter-metrics-otlp-proto": "0.214.0", + "@opentelemetry/exporter-prometheus": "0.214.0", + "@opentelemetry/exporter-trace-otlp-grpc": "0.214.0", + "@opentelemetry/exporter-trace-otlp-http": "0.214.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.214.0", + "@opentelemetry/exporter-zipkin": "2.6.1", + "@opentelemetry/instrumentation": "0.214.0", + "@opentelemetry/otlp-exporter-base": "0.214.0", + "@opentelemetry/propagator-b3": "2.6.1", + "@opentelemetry/propagator-jaeger": "2.6.1", + "@opentelemetry/resources": "2.6.1", + "@opentelemetry/sdk-logs": "0.214.0", + "@opentelemetry/sdk-metrics": "2.6.1", + "@opentelemetry/sdk-trace-base": "2.6.1", + "@opentelemetry/sdk-trace-node": "2.6.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/sdk-trace-base": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.1.tgz", @@ -3032,6 +3927,23 @@ "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.6.1.tgz", + "integrity": "sha512-Hh2i4FwHWRFhnO2Q/p6svMxy8MPsNCG0uuzUY3glqm0rwM0nQvbTO1dXSp9OqQoTKXcQzaz9q1f65fsurmOhNw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-async-hooks": "2.6.1", + "@opentelemetry/core": "2.6.1", + "@opentelemetry/sdk-trace-base": "2.6.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, "node_modules/@opentelemetry/semantic-conventions": { "version": "1.40.0", "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", @@ -3075,6 +3987,13 @@ "node": ">=14" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, "node_modules/@prisma/client": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.18.0.tgz", @@ -3213,6 +4132,70 @@ "module-details-from-path": "^1.0.4" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -4973,6 +5956,12 @@ "license": "MIT", "peer": true }, + "node_modules/@types/aws-lambda": { + "version": "8.10.161", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.161.tgz", + "integrity": "sha512-rUYdp+MQwSFocxIOcSsYSF3YYYC/uUpMbCY/mbO21vGqfrEYvNSoPyKYDj6RhXXpPfS0KstW9RwG3qXh9sL7FQ==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -5018,6 +6007,15 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/bunyan": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/@types/bunyan/-/bunyan-1.8.11.tgz", + "integrity": "sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -5133,6 +6131,15 @@ "@types/unist": "*" } }, + "node_modules/@types/memcached": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/@types/memcached/-/memcached-2.2.10.tgz", + "integrity": "sha512-AM9smvZN55Gzs2wRrqeMHVP7KE8KWgCJO/XL5yCly2xF6EKa4YlbpK+cLSAH4NG/Ah64HrlegmGqW8kYws7Vxg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -5157,10 +6164,20 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/oracledb": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@types/oracledb/-/oracledb-6.5.2.tgz", + "integrity": "sha512-kK1eBS/Adeyis+3OlBDMeQQuasIDLUYXsi2T15ccNJ0iyUpQ4xDF7svFu3+bGVrI0CMBUclPciz+lsQR3JX3TQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/papaparse": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.16.tgz", - "integrity": "sha512-T3VuKMC2H0lgsjI9buTB3uuKj3EMD2eap1MOuEQuBQ44EnDx/IkGhU6EwiTf9zG3za4SKlmwKAImdDKdNnCsXg==", + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.5.2.tgz", + "integrity": "sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -6191,6 +7208,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -6669,6 +7699,15 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -6974,6 +8013,15 @@ "pnpm": ">=8" } }, + "node_modules/chartjs-plugin-datalabels": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-2.2.0.tgz", + "integrity": "sha512-14ZU30lH7n89oq+A4bWaJPnAG8a7ZTk7dKf48YAzMvJjQtjrgg5Dpk9f+LbjCF6bpx3RAGTeL13IXpKQYyRvlw==", + "license": "MIT", + "peerDependencies": { + "chart.js": ">=3.0.0" + } + }, "node_modules/check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -7077,6 +8125,66 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -7239,6 +8347,15 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/data-urls": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", @@ -7317,6 +8434,13 @@ "url": "https://github.com/sponsors/kossnocorp" } }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -7591,6 +8715,13 @@ "node": ">= 0.4" } }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true, + "license": "MIT" + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -8765,6 +9896,29 @@ } } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/fflate": { "version": "0.4.8", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", @@ -8909,6 +10063,18 @@ "node": ">=0.4.x" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded-parse": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", @@ -8930,13 +10096,13 @@ } }, "node_modules/framer-motion": { - "version": "12.23.24", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz", - "integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==", + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", + "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==", "license": "MIT", "dependencies": { - "motion-dom": "^12.23.23", - "motion-utils": "^12.23.6", + "motion-dom": "^12.38.0", + "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { @@ -9010,6 +10176,65 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gaxios/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -9029,6 +10254,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-east-asian-width": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", @@ -9150,18 +10384,6 @@ "giget": "dist/cli.mjs" } }, - "node_modules/github-markdown-css": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/github-markdown-css/-/github-markdown-css-5.8.1.tgz", - "integrity": "sha512-8G+PFvqigBQSWLQjyzgpa2ThD9bo7+kDsriUIidGcRhXgmcaAWUIpCZf8DavJgc+xifjbCG+GvMyWr0XMXmc7g==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/glob": { "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", @@ -9276,6 +10498,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -9295,6 +10526,22 @@ "license": "ISC", "peer": true }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -9500,6 +10747,13 @@ "node": ">=18" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -10011,6 +11265,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -10382,6 +11646,15 @@ "node": ">=6" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -10644,6 +11917,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -10788,6 +12067,12 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -11897,21 +13182,57 @@ "node": ">= 18" } }, - "node_modules/motion-dom": { - "version": "12.23.23", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", - "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "node_modules/motion": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.38.0.tgz", + "integrity": "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==", "license": "MIT", "dependencies": { - "motion-utils": "^12.23.6" + "framer-motion": "^12.38.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", + "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.36.0" } }, "node_modules/motion-utils": { - "version": "12.23.6", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", - "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", + "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", "license": "MIT" }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -12078,6 +13399,26 @@ "node": "^18 || ^20 || >= 21" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -12412,6 +13753,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, "node_modules/openid-client": { "version": "6.8.2", "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.8.2.tgz", @@ -13154,6 +14505,30 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -13334,20 +14709,6 @@ "react": ">=18" } }, - "node_modules/react-papaparse": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/react-papaparse/-/react-papaparse-4.4.0.tgz", - "integrity": "sha512-xTEwHZYJ+1dh9mQDQjjwJXmWyX20DdZ52u+ddw75V+Xm5qsjXSvWmC7c8K82vRwMjKAOH2S9uFyGpHEyEztkUQ==", - "license": "MIT", - "dependencies": { - "@types/papaparse": "^5.3.9", - "papaparse": "^5.4.1" - }, - "engines": { - "node": ">=8", - "npm": ">=5" - } - }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -13652,6 +15013,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -14236,6 +15606,21 @@ "simple-git-hooks": "cli.js" } }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/slice-ansi": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", @@ -15176,6 +16561,16 @@ "node": ">=8.0" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/tough-cookie": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", @@ -15953,6 +17348,15 @@ "node": ">=10.13.0" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/web-tree-sitter": { "version": "0.26.5", "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.26.5.tgz", @@ -16018,6 +17422,66 @@ } } }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.1.tgz", + "integrity": "sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "is-plain-object": "^5.0.0", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/webpack-sources": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", @@ -16397,6 +17861,15 @@ "node": ">=0.4" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -16407,7 +17880,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "devOptional": true, "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -16416,6 +17888,62 @@ "node": ">= 14.6" } }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/js/cf-webapp/package.json b/js/cf-webapp/package.json index 8285f1ddf..0322e04a5 100644 --- a/js/cf-webapp/package.json +++ b/js/cf-webapp/package.json @@ -11,6 +11,7 @@ "lint:check": "eslint .", "test": "vitest", "type-check": "tsc --noEmit", + "analyze": "ANALYZE=true next build", "prisma:generate": "npx prisma generate", "prisma:migrate": "npx prisma migrate dev", "prepare": "simple-git-hooks", @@ -21,11 +22,13 @@ "dependencies": { "@anthropic-ai/sdk": "^0.74.0", "@auth0/nextjs-auth0": "^4", - "@azure/msal-node": "^3.7.3", "@codeflash-ai/common": "^1.0.30", "@hookform/resolvers": "^3.3.2", "@monaco-editor/react": "^4.7.0", + "@opentelemetry/auto-instrumentations-node": "^0.72.0", + "@opentelemetry/sdk-node": "^0.214.0", "@prisma/client": "^6.7.0", + "@prisma/instrumentation": "^7.6.0", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", @@ -38,25 +41,27 @@ "@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-tooltip": "^1.1.4", "@sentry/nextjs": "^10.38.0", + "@sentry/opentelemetry": "^10.47.0", "@types/node": "^24.3.0", "@types/pg": "^8.10.9", "@types/react": "19.2.13", "@types/react-dom": "19.2.3", "@types/react-syntax-highlighter": "^15.5.13", "chart.js": "^4.4.9", + "chartjs-plugin-datalabels": "^2.2.0", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "date-fns": "^4.1.0", "diff": "^8.0.2", - "framer-motion": "^12.12.1", - "github-markdown-css": "^5.4.0", "jsonwebtoken": "^9.0.2", "lucide-react": "^0.563.0", "marked": "^16.1.1", + "motion": "^12.38.0", "next": "16.1.6", "next-themes": "^0.4.6", "node-ts-cache": "^4.4.0", "node-ts-cache-storage-memory": "^4.4.0", + "papaparse": "^5.5.3", "pg": "^8.11.3", "postcss": "^8", "posthog-js": "1.127.0", @@ -67,7 +72,6 @@ "react-dom": "19.2.4", "react-hook-form": "^7.48.2", "react-markdown": "^9.0.1", - "react-papaparse": "^4.4.0", "react-resizable-panels": "^4.6.4", "react-syntax-highlighter": "^16.1.0", "remark-gfm": "^4.0.0", @@ -80,8 +84,10 @@ "zod": "^3.22.4" }, "devDependencies": { + "@next/bundle-analyzer": "^16.2.2", "@testing-library/react": "^16.0.0", "@types/jsonwebtoken": "^9.0.10", + "@types/papaparse": "^5.5.2", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.0.1", "baseline-browser-mapping": "^2.9.11", diff --git a/js/cf-webapp/roadmap.png b/js/cf-webapp/roadmap.png new file mode 100644 index 000000000..24278c48b Binary files /dev/null and b/js/cf-webapp/roadmap.png differ diff --git a/js/cf-webapp/sentry.server.config.ts b/js/cf-webapp/sentry.server.config.ts index e4b4f832b..621ae4241 100644 --- a/js/cf-webapp/sentry.server.config.ts +++ b/js/cf-webapp/sentry.server.config.ts @@ -11,8 +11,10 @@ Sentry.init({ ? "https://0fa0f40b2d709e4f1eb9aac76ff9e6be@o4506833230561280.ingest.us.sentry.io/4506833279582208" : undefined, - // Adjust this value in production, or use tracesSampler for greater control - tracesSampleRate: 1, + tracesSampleRate: isProduction ? 0.1 : 1, + + // Let the custom OTel setup in src/instrumentation.ts manage OpenTelemetry + skipOpenTelemetrySetup: true, // Setting this option to true will print useful information to the console while you're setting up Sentry. debug: false, diff --git a/js/cf-webapp/src/app/(auth)/onboarding/SubmitFirstOnboardingPage.tsx b/js/cf-webapp/src/app/(auth)/onboarding/SubmitFirstOnboardingPage.tsx index 8877867d7..944c8f747 100644 --- a/js/cf-webapp/src/app/(auth)/onboarding/SubmitFirstOnboardingPage.tsx +++ b/js/cf-webapp/src/app/(auth)/onboarding/SubmitFirstOnboardingPage.tsx @@ -39,7 +39,7 @@ export async function SubmitFirstOnboardingPage( custom_pain_point: customOptionInput, }, }) - await posthog?.shutdown() + await posthog?.flush() await submitOnboardingQuestions(user_id, email) // Check for saved redirect URL after onboarding completion @@ -81,7 +81,7 @@ export async function SubmitSkipOnboardingPage(): Promise { username: nickname, }, }) - await posthog?.shutdown() + await posthog?.flush() await markUserCompletedOnboarding(user_id) // Checking for saved redirect URL after onboarding completion diff --git a/js/cf-webapp/src/app/(auth)/onboarding/SubmitSecondOnboardingPage.tsx b/js/cf-webapp/src/app/(auth)/onboarding/SubmitSecondOnboardingPage.tsx index fbbae9751..4d824be87 100644 --- a/js/cf-webapp/src/app/(auth)/onboarding/SubmitSecondOnboardingPage.tsx +++ b/js/cf-webapp/src/app/(auth)/onboarding/SubmitSecondOnboardingPage.tsx @@ -31,5 +31,5 @@ export async function SubmitSecondOnboardingPage( ...(colleagueInviteEmail && { colleague_invite_email: colleagueInviteEmail }), }, }) - await posthog?.shutdown() + await posthog?.flush() } diff --git a/js/cf-webapp/src/app/(auth)/onboarding/page.tsx b/js/cf-webapp/src/app/(auth)/onboarding/page.tsx index ae2f58198..b14af9d6e 100644 --- a/js/cf-webapp/src/app/(auth)/onboarding/page.tsx +++ b/js/cf-webapp/src/app/(auth)/onboarding/page.tsx @@ -2,7 +2,7 @@ import { useMemo, useState, useEffect, type ReactNode } from "react" import { useRouter } from "next/navigation" -import { AnimatePresence, motion } from "framer-motion" +import { AnimatePresence, motion } from "motion/react" import { ArrowRight, ArrowRightCircle, diff --git a/js/cf-webapp/src/app/(dashboard)/apikeys/page.tsx b/js/cf-webapp/src/app/(dashboard)/apikeys/page.tsx index 4c9aabddc..0b6391cc5 100644 --- a/js/cf-webapp/src/app/(dashboard)/apikeys/page.tsx +++ b/js/cf-webapp/src/app/(dashboard)/apikeys/page.tsx @@ -4,11 +4,10 @@ import { auth0 } from "@/lib/auth0" import { CreateApiKeyDialog } from "./dialog-create-api-key" import { Separator } from "@/components/ui/separator" import { ApiKeyTable } from "./api-key-table" -import { type cf_api_keys, PrismaClient } from "@prisma/client" +import { type cf_api_keys } from "@prisma/client" import PostHogClient from "@/lib/posthog" import { VS_CODE_KEY_NAME } from "@codeflash-ai/common" - -const prisma = new PrismaClient() +import { prisma } from "@/lib/prisma" interface ApiKeyWithOrg extends cf_api_keys { organization?: { @@ -41,10 +40,7 @@ export default async function APIKeyGenerator(): Promise { // Fetch personal keys (no organization) and keys from user's organizations const apiKeys: ApiKeyWithOrg[] = await prisma.cf_api_keys.findMany({ where: { - OR: [ - { user_id: userId, organization_id: null }, - { organization_id: { in: userOrgIds } }, - ], + OR: [{ user_id: userId, organization_id: null }, { organization_id: { in: userOrgIds } }], }, include: { organization: { @@ -69,7 +65,7 @@ export default async function APIKeyGenerator(): Promise { event: "webapp-loaded-api-keys", }) - await posthog?.shutdown() + await posthog?.flush() return (
diff --git a/js/cf-webapp/src/app/(dashboard)/apikeys/tokenfuncs.ts b/js/cf-webapp/src/app/(dashboard)/apikeys/tokenfuncs.ts index a8935becd..eb583b53e 100644 --- a/js/cf-webapp/src/app/(dashboard)/apikeys/tokenfuncs.ts +++ b/js/cf-webapp/src/app/(dashboard)/apikeys/tokenfuncs.ts @@ -8,9 +8,8 @@ import { VS_CODE_KEY_NAME, } from "@codeflash-ai/common" import { TokenLimitExceededError } from "./token-error" -import { PrismaClient } from "@prisma/client" - -const prisma = new PrismaClient() +import { prisma } from "@/lib/prisma" +import { trackApiKeyCreated } from "@/lib/analytics/tracking" export async function generateToken( keyName: string, @@ -24,12 +23,16 @@ export async function generateToken( try { const token: string = await safeGenAndStoreAPITokenHash(keyName, userId, organizationId) + await trackApiKeyCreated(userId, { keyName, organizationId }) return { success: true, token, err: undefined } } catch (error) { if (error instanceof Error && error.message === "Token limit exceeded") { return { success: false, err: new TokenLimitExceededError().message, token: undefined } } - if (error instanceof Error && error.message === "User is not a member of the specified organization") { + if ( + error instanceof Error && + error.message === "User is not a member of the specified organization" + ) { return { success: false, err: error.message, token: undefined } } return { diff --git a/js/cf-webapp/src/app/(dashboard)/billing/page.tsx b/js/cf-webapp/src/app/(dashboard)/billing/page.tsx index c78cd3755..44603df12 100644 --- a/js/cf-webapp/src/app/(dashboard)/billing/page.tsx +++ b/js/cf-webapp/src/app/(dashboard)/billing/page.tsx @@ -1,7 +1,7 @@ "use server" import { auth0 } from "@/lib/auth0" import { BillingView } from "./billing-view" -import PostHogClient from "@/lib/posthog" +import { trackBillingPageViewed } from "@/lib/analytics/tracking" import { SUBSCRIPTION_PLANS, checkAndResetSubscriptionPeriod } from "@codeflash-ai/common" export default async function BillingPage() { @@ -10,13 +10,7 @@ export default async function BillingPage() { const userId = session.user.sub try { // Track page view - const posthog = PostHogClient() - posthog?.capture({ - distinctId: userId, - properties: { username: session.user.nickname }, - event: "webapp-loaded-billing-page", - }) - await posthog?.shutdown() + await trackBillingPageViewed(userId, { username: session.user.nickname }) // Get subscription info from database with lazy reset const subscription = (await checkAndResetSubscriptionPeriod(userId)) || { diff --git a/js/cf-webapp/src/app/(dashboard)/getting-started/page.tsx b/js/cf-webapp/src/app/(dashboard)/getting-started/page.tsx index 197dde90d..145be66c0 100644 --- a/js/cf-webapp/src/app/(dashboard)/getting-started/page.tsx +++ b/js/cf-webapp/src/app/(dashboard)/getting-started/page.tsx @@ -14,7 +14,7 @@ export default async function GettingStarted() { event: "webapp-loaded-getting-started", }) - await posthog?.shutdown() + await posthog?.flush() return } diff --git a/js/cf-webapp/src/app/(dashboard)/members/__tests__/action.test.ts b/js/cf-webapp/src/app/(dashboard)/members/__tests__/action.test.ts new file mode 100644 index 000000000..c46ea88ed --- /dev/null +++ b/js/cf-webapp/src/app/(dashboard)/members/__tests__/action.test.ts @@ -0,0 +1,118 @@ +import { describe, it, expect, vi, beforeEach } from "vitest" +import { prisma } from "@codeflash-ai/common" + +vi.mock("@/lib/server-action-timing", () => ({ + withTiming: vi.fn((_name: string, fn: Function) => fn), +})) + +vi.mock("@/lib/analytics/tracking", () => ({ + trackMemberInvited: vi.fn(), +})) + +const mockOrg = { + id: "org-1", + organization_members: [ + { + id: "member-1", + user_id: "user-1", + role: "admin", + added_at: new Date("2024-01-15"), + user: { + github_username: "alice", + name: "Alice Smith", + email: "alice@example.com", + }, + }, + { + id: "member-2", + user_id: "user-2", + role: "member", + added_at: new Date("2024-02-01"), + user: { + github_username: "bob", + name: "Bob Jones", + email: "bob@example.com", + }, + }, + ], +} + +describe("getOrganizationMembers", () => { + let getOrganizationMembers: typeof import("../action").getOrganizationMembers + + beforeEach(async () => { + const mod = await import("../action") + getOrganizationMembers = mod.getOrganizationMembers + }) + + describe("successful retrieval", () => { + it("returns members when user has access", async () => { + vi.mocked(prisma.organizations.findFirst).mockResolvedValue(mockOrg as any) + + const result = await getOrganizationMembers("user-1", "org-1") + + expect(result.success).toBe(true) + expect(result.data).toHaveLength(2) + }) + + it("maps nested organization_members to flat Member structure", async () => { + vi.mocked(prisma.organizations.findFirst).mockResolvedValue(mockOrg as any) + + const result = await getOrganizationMembers("user-1", "org-1") + const member = result.data![0] + + expect(member).toEqual({ + id: "member-1", + user_id: "user-1", + username: "alice", + name: "Alice Smith", + email: "alice@example.com", + role: "admin", + added_at: new Date("2024-01-15"), + avatarUrl: "https://github.com/alice.png", + }) + }) + }) + + describe("access control", () => { + it("returns error when organization not found", async () => { + vi.mocked(prisma.organizations.findFirst).mockResolvedValue(null) + + const result = await getOrganizationMembers("user-1", "org-1") + + expect(result.success).toBe(false) + expect(result.error).toBe("Organization not found") + }) + + it("returns error when user is not in organization members", async () => { + vi.mocked(prisma.organizations.findFirst).mockResolvedValue(mockOrg as any) + + const result = await getOrganizationMembers("unknown-user", "org-1") + + expect(result.success).toBe(false) + expect(result.error).toBe("You don't have access to this organization") + }) + }) + + describe("error handling", () => { + it("returns error response when Prisma throws", async () => { + vi.mocked(prisma.organizations.findFirst).mockRejectedValue( + new Error("Connection failed"), + ) + + const result = await getOrganizationMembers("user-1", "org-1") + + expect(result.success).toBe(false) + expect(result.error).toBe("Connection failed") + }) + + it("uses fallback message for non-Error exceptions", async () => { + vi.mocked(prisma.organizations.findFirst).mockRejectedValue("string error") + + const result = await getOrganizationMembers("user-1", "org-1") + + expect(result.success).toBe(false) + expect(result.error).toBe("Failed to get members") + }) + }) +}) diff --git a/js/cf-webapp/src/app/(dashboard)/members/action.ts b/js/cf-webapp/src/app/(dashboard)/members/action.ts index bebdf1788..3ca73f0d3 100644 --- a/js/cf-webapp/src/app/(dashboard)/members/action.ts +++ b/js/cf-webapp/src/app/(dashboard)/members/action.ts @@ -8,14 +8,18 @@ import { organizationMemberRepository, prisma, } from "@codeflash-ai/common" +import { withTiming } from "@/lib/server-action-timing" +import { trackMemberInvited } from "@/lib/analytics/tracking" /** * Get organization members */ -export async function getOrganizationMembers( - currentUserId: string, - organizationId: string, -): Promise> { +export const getOrganizationMembers = withTiming( + "getOrganizationMembers", + async ( + currentUserId: string, + organizationId: string, + ): Promise> => { try { const org = await prisma.organizations.findFirst({ where: { id: organizationId }, @@ -58,7 +62,8 @@ export async function getOrganizationMembers( console.error("Failed to get organization members:", error) return createErrorResponse(error instanceof Error ? error.message : "Failed to get members") } -} + }, +) /** * Add a member to organization @@ -121,6 +126,14 @@ export async function addOrganizationMember( added_by: currentUserId, }, }) + + trackMemberInvited(currentUserId, { + invitedUsername: invitedUser.username, + role, + scope: "organization", + targetId: organizationId, + }) + return createSuccessResponse({ id: newMember.id, user_id: newMember.user_id, diff --git a/js/cf-webapp/src/app/(dashboard)/members/page.tsx b/js/cf-webapp/src/app/(dashboard)/members/page.tsx index 12dc7a649..3c5b49774 100644 --- a/js/cf-webapp/src/app/(dashboard)/members/page.tsx +++ b/js/cf-webapp/src/app/(dashboard)/members/page.tsx @@ -59,13 +59,14 @@ function OrganizationMembers() { setCurrentUserId(data.userId) - const roleResult = await getCurrentUserRole(data.userId, currentOrg?.id) + const [roleResult, result] = await Promise.all([ + getCurrentUserRole(data.userId, currentOrg?.id), + getOrganizationMembers(data.userId, currentOrg?.id), + ]) if (roleResult.success && roleResult.data) { setCurrentUserRole(roleResult.data.role) } - const result = await getOrganizationMembers(data.userId, currentOrg?.id) - if (result.success && result.data) { setMembers(result.data) } else { @@ -103,10 +104,7 @@ function OrganizationMembers() { setSuccess("Member added successfully!") } - const handleUserAdd = async ( - user: GitHubUserSearchResult, - role: "admin" | "member", - ) => { + const handleUserAdd = async (user: GitHubUserSearchResult, role: "admin" | "member") => { if (!currentOrg?.id) { return { success: false, error: "No organization selected" } } diff --git a/js/cf-webapp/src/app/(dashboard)/repositories/[repositoryId]/__tests__/action.test.ts b/js/cf-webapp/src/app/(dashboard)/repositories/[repositoryId]/__tests__/action.test.ts new file mode 100644 index 000000000..adbf9906c --- /dev/null +++ b/js/cf-webapp/src/app/(dashboard)/repositories/[repositoryId]/__tests__/action.test.ts @@ -0,0 +1,163 @@ +import { describe, it, expect, vi, beforeEach } from "vitest" +import { prisma } from "@codeflash-ai/common" +import { getRepositoriesForAccountCached } from "@/lib/services/repository-utils" +import { trackRepositoryConnected } from "@/lib/analytics/tracking" + +vi.mock("@/lib/server-action-timing", () => ({ + withTiming: vi.fn((_name: string, fn: Function) => fn), +})) + +vi.mock("@/lib/services/repository-utils", () => ({ + getRepositoriesForAccountCached: vi.fn(), +})) + +vi.mock("@/lib/analytics/tracking", () => ({ + trackMemberInvited: vi.fn(), + trackRepositoryConnected: vi.fn(), +})) + +const mockRepo = { + id: "repo-1", + github_repo_id: "12345", + name: "my-repo", + full_name: "myorg/my-repo", + is_private: false, + has_github_action: true, + created_at: new Date("2024-01-01"), + last_optimized: new Date("2024-06-01"), + optimizations_limit: 100, + optimizations_used: 50, + repository_members: [{ id: "rm-1" }, { id: "rm-2" }], +} + +const mockPayload = { userId: "user-1", username: "testuser" } + +describe("getRepositoryById", () => { + let getRepositoryById: typeof import("../action").getRepositoryById + + beforeEach(async () => { + const mod = await import("../action") + getRepositoryById = mod.getRepositoryById + }) + + describe("parallel fetch", () => { + it("fetches repo and authorized repoIds concurrently", async () => { + vi.mocked(prisma.repositories.findFirst).mockResolvedValue(mockRepo as any) + vi.mocked(getRepositoriesForAccountCached).mockResolvedValue({ + repoIds: ["repo-1"], + repos: [], + } as any) + vi.mocked(prisma.optimization_events.count).mockResolvedValue(5) + + await getRepositoryById(mockPayload as any, "repo-1") + + expect(prisma.repositories.findFirst).toHaveBeenCalledTimes(1) + expect(getRepositoriesForAccountCached).toHaveBeenCalledWith(mockPayload) + }) + + it("returns null when repo is not found", async () => { + vi.mocked(prisma.repositories.findFirst).mockResolvedValue(null) + vi.mocked(getRepositoriesForAccountCached).mockResolvedValue({ + repoIds: ["repo-1"], + repos: [], + } as any) + + const result = await getRepositoryById(mockPayload as any, "repo-1") + expect(result).toBeNull() + }) + + it("returns null when repo is not in authorized list", async () => { + vi.mocked(prisma.repositories.findFirst).mockResolvedValue(mockRepo as any) + vi.mocked(getRepositoriesForAccountCached).mockResolvedValue({ + repoIds: ["other-repo"], + repos: [], + } as any) + + const result = await getRepositoryById(mockPayload as any, "repo-1") + expect(result).toBeNull() + }) + }) + + describe("successful retrieval", () => { + beforeEach(() => { + vi.mocked(prisma.repositories.findFirst).mockResolvedValue(mockRepo as any) + vi.mocked(getRepositoriesForAccountCached).mockResolvedValue({ + repoIds: ["repo-1"], + repos: [], + } as any) + }) + + it("returns RepositoryWithUsage with all required fields", async () => { + vi.mocked(prisma.optimization_events.count).mockResolvedValue(3) + + const result = await getRepositoryById(mockPayload as any, "repo-1") + + expect(result).toEqual({ + id: "repo-1", + github_repo_id: "12345", + name: "my-repo", + full_name: "myorg/my-repo", + is_private: false, + is_active: true, + has_github_action: true, + created_at: new Date("2024-01-01"), + last_optimized: new Date("2024-06-01"), + optimizations_limit: 100, + optimizations_used: 50, + organization: "myorg", + avatarUrl: "https://github.com/myorg.png", + membersCount: 2, + }) + }) + + it("sets is_active to false when no recent events", async () => { + vi.mocked(prisma.optimization_events.count).mockResolvedValue(0) + + const result = await getRepositoryById(mockPayload as any, "repo-1") + expect(result!.is_active).toBe(false) + }) + + it("sets is_active to true when recent events exist", async () => { + vi.mocked(prisma.optimization_events.count).mockResolvedValue(10) + + const result = await getRepositoryById(mockPayload as any, "repo-1") + expect(result!.is_active).toBe(true) + }) + }) + + describe("analytics tracking", () => { + beforeEach(() => { + vi.mocked(prisma.repositories.findFirst).mockResolvedValue(mockRepo as any) + vi.mocked(getRepositoriesForAccountCached).mockResolvedValue({ + repoIds: ["repo-1"], + repos: [], + } as any) + vi.mocked(prisma.optimization_events.count).mockResolvedValue(1) + }) + + it("calls trackRepositoryConnected for user payloads", async () => { + await getRepositoryById(mockPayload as any, "repo-1") + + expect(trackRepositoryConnected).toHaveBeenCalledWith("user-1", { + repositoryId: "repo-1", + repositoryName: "myorg/my-repo", + }) + }) + }) + + describe("error handling", () => { + it("returns null and logs when Prisma throws", async () => { + vi.spyOn(console, "error").mockImplementation(() => {}) + vi.mocked(prisma.repositories.findFirst).mockRejectedValue( + new Error("timeout"), + ) + vi.mocked(getRepositoriesForAccountCached).mockResolvedValue({ + repoIds: ["repo-1"], + repos: [], + } as any) + + const result = await getRepositoryById(mockPayload as any, "repo-1") + expect(result).toBeNull() + }) + }) +}) diff --git a/js/cf-webapp/src/app/(dashboard)/repositories/[repositoryId]/action.ts b/js/cf-webapp/src/app/(dashboard)/repositories/[repositoryId]/action.ts index aa670b5d2..b1e861d7e 100644 --- a/js/cf-webapp/src/app/(dashboard)/repositories/[repositoryId]/action.ts +++ b/js/cf-webapp/src/app/(dashboard)/repositories/[repositoryId]/action.ts @@ -1,12 +1,14 @@ "use server" -import * as Sentry from "@sentry/node" +import * as Sentry from "@sentry/nextjs" import { AccountPayload, createOrUpdateUser, getUserById, prisma } from "@codeflash-ai/common" import { eachDayOfInterval, startOfDay } from "date-fns" import { GitHubUserSearchResult, Member, UserRole } from "@/lib/types" import { ActionResponse, createErrorResponse, createSuccessResponse } from "@/lib/action-response" import { RepositoryWithUsage } from "@/app/dashboard/action" import { getRepositoriesForAccountCached } from "@/lib/services/repository-utils" +import { withTiming } from "@/lib/server-action-timing" +import { trackMemberInvited, trackRepositoryConnected } from "@/lib/analytics/tracking" export async function getOptimizationsTimeSeriesData(repoId: string, onlySuccessful?: boolean) { try { @@ -158,53 +160,61 @@ export async function getActiveUserLeaderboardLast30DaysForRepo( })) } -export async function getRepositoryById( - payload: AccountPayload, - repoId: string, -): Promise { - try { - const repo = await prisma.repositories.findFirst({ - where: { - id: repoId, - }, - include: { - repository_members: true, - }, - }) - const repoIds = await (await getRepositoriesForAccountCached(payload)).repoIds +export const getRepositoryById = withTiming( + "getRepositoryById", + async (payload: AccountPayload, repoId: string): Promise => { + try { + // Fetch repo and authorized repoIds in parallel + const [repo, { repoIds }] = await Promise.all([ + prisma.repositories.findFirst({ + where: { id: repoId }, + include: { repository_members: true }, + }), + getRepositoriesForAccountCached(payload), + ]) - if (!repo || !repoIds.includes(repo.id)) return null + if (!repo || !repoIds.includes(repo.id)) return null - const recentEventCount = await prisma.optimization_events.count({ - where: { - repository_id: repo.id, - created_at: { - gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), + const recentEventCount = await prisma.optimization_events.count({ + where: { + repository_id: repo.id, + created_at: { + gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), + }, }, - }, - }) + }) - return { - id: repo.id, - github_repo_id: repo.github_repo_id, - name: repo.name, - full_name: repo.full_name, - is_private: repo.is_private, - is_active: recentEventCount > 0, - has_github_action: repo.has_github_action, - created_at: repo.created_at, - last_optimized: repo.last_optimized, - optimizations_limit: repo.optimizations_limit, - optimizations_used: repo.optimizations_used, - organization: repo.full_name.split("/")[0], - avatarUrl: `https://github.com/${repo.full_name.split("/")[0]}.png`, - membersCount: repo.repository_members.length, + // Track repository view as a connection/engagement signal + const userId = "userId" in payload ? payload.userId : undefined + if (userId) { + trackRepositoryConnected(userId, { + repositoryId: repo.id, + repositoryName: repo.full_name, + }) + } + + return { + id: repo.id, + github_repo_id: repo.github_repo_id, + name: repo.name, + full_name: repo.full_name, + is_private: repo.is_private, + is_active: recentEventCount > 0, + has_github_action: repo.has_github_action, + created_at: repo.created_at, + last_optimized: repo.last_optimized, + optimizations_limit: repo.optimizations_limit, + optimizations_used: repo.optimizations_used, + organization: repo.full_name.split("/")[0], + avatarUrl: `https://github.com/${repo.full_name.split("/")[0]}.png`, + membersCount: repo.repository_members.length, + } + } catch (error) { + console.error("Failed to fetch repository by ID:", error) + return null } - } catch (error) { - console.error("Failed to fetch repository by ID:", error) - return null - } -} + }, +) export async function addRepositoryMemberById( currentUserId: string, @@ -265,6 +275,13 @@ export async function addRepositoryMemberById( }, }) + trackMemberInvited(currentUserId, { + invitedUsername: invitedUser.username, + role, + scope: "repository", + targetId: repoId, + }) + return createSuccessResponse({ id: newMember.id, user_id: newMember.user_id, diff --git a/js/cf-webapp/src/app/(dashboard)/repositories/[repositoryId]/page.tsx b/js/cf-webapp/src/app/(dashboard)/repositories/[repositoryId]/page.tsx index a701fcb3d..9e982f891 100644 --- a/js/cf-webapp/src/app/(dashboard)/repositories/[repositoryId]/page.tsx +++ b/js/cf-webapp/src/app/(dashboard)/repositories/[repositoryId]/page.tsx @@ -576,9 +576,22 @@ function RepositoryDetail() { setRepository(currentRepo) - const totalAttempts = await getUserOptimizationCountByRepo(repositoryId) - const successfulAttempts = await getUserOptimizationSuccessfulCountByRepo(repositoryId) - const optimizationsOverTime = await getOptimizationsTimeSeriesData(repositoryId, false) + // Fetch all statistics in parallel - these are all independent queries + const [ + totalAttempts, + successfulAttempts, + optimizationsOverTime, + successfulOptimizationsOverTime, + prData, + leaderboardData, + ] = await Promise.all([ + getUserOptimizationCountByRepo(repositoryId), + getUserOptimizationSuccessfulCountByRepo(repositoryId), + getOptimizationsTimeSeriesData(repositoryId, false), + getOptimizationsTimeSeriesData(repositoryId, true), + getPullRequestEventTimeSeriesData(selectedPrYear, repositoryId), + getActiveUserLeaderboardLast30DaysForRepo(repositoryId), + ]) if (Array.isArray(optimizationsOverTime) && optimizationsOverTime.length > 0) { const optimizationValues = optimizationsOverTime.map(item => item?.count || 0) @@ -590,11 +603,6 @@ function RepositoryDetail() { setOptimizationsTrendDates([]) } - const successfulOptimizationsOverTime = await getOptimizationsTimeSeriesData( - repositoryId, - true, - ) - if ( Array.isArray(successfulOptimizationsOverTime) && successfulOptimizationsOverTime.length > 0 @@ -608,16 +616,12 @@ function RepositoryDetail() { setSuccessfulOptimizationsTrendDates([]) } - const prData = await getPullRequestEventTimeSeriesData(selectedPrYear, repositoryId) - if (Array.isArray(prData)) { setPrActivityData(prData) } else { setPrActivityData([]) } - const leaderboardData = await getActiveUserLeaderboardLast30DaysForRepo(repositoryId) - if (Array.isArray(leaderboardData)) { setActiveUsersData(leaderboardData) } else { diff --git a/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/action.ts b/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/action.ts index 96faba89c..114578569 100644 --- a/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/action.ts +++ b/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/action.ts @@ -6,6 +6,7 @@ import { getRepositoriesForAccountCached } from "@/lib/services/repository-utils import { auth0 } from "@/lib/auth0" import { AccountPayload, buildOptimizationOrCondition, prisma } from "@codeflash-ai/common" import * as Sentry from "@sentry/nextjs" +import { trackOptimizationReviewed } from "@/lib/analytics/tracking" export interface DiffContent { oldContent: string @@ -156,31 +157,44 @@ export async function getOptimizationEventById({ trace_id, ...buildOptimizationOrCondition(payload, repoIds), } - const event = await prisma.optimization_events.findFirst({ - where, - include: { - repository: true, - }, - }) - if (event) { - // Fetch review_quality and review_explanation from optimization_features - const features = await prisma.optimization_features.findUnique({ - where: { trace_id: event.trace_id }, + // Fire both queries in parallel — features only needs trace_id, not the event result + const [event, features] = await Promise.all([ + prisma.optimization_events.findFirst({ + where, + include: { + repository: true, + }, + }), + prisma.optimization_features.findUnique({ + where: { trace_id }, select: { review_quality: true, review_explanation: true, }, - }) + }), + ]) - return { - ...event, - review_quality: features?.review_quality || null, - review_explanation: features?.review_explanation || null, - } + if (!event) { + return null } - return event + // Track that this optimization was reviewed + const userId = "userId" in payload ? payload.userId : undefined + if (userId) { + trackOptimizationReviewed(userId, { + traceId: event.trace_id, + functionName: event.function_name, + repositoryName: event.repository?.full_name ?? null, + status: event.status, + }) + } + + return { + ...event, + review_quality: features?.review_quality || null, + review_explanation: features?.review_explanation || null, + } } export async function saveOptimizationChanges({ eventId, diff --git a/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/profiler/page.tsx b/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/profiler/page.tsx index 8262b2265..71b557077 100644 --- a/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/profiler/page.tsx +++ b/js/cf-webapp/src/app/(dashboard)/review-optimizations/[traceId]/profiler/page.tsx @@ -5,7 +5,16 @@ import { useParams, useRouter } from "next/navigation" import { ArrowLeft, Zap, Loader2, AlertTriangle } from "lucide-react" import { getOptimizationEventById } from "../action" import { getUserIdAndUsername } from "@/app/utils/auth" -import { LineProfilerView } from "@/components/LineProfiler" +import dynamic from "next/dynamic" +import { Skeleton } from "@/components/ui/skeleton" + +const LineProfilerView = dynamic( + () => import("@/components/LineProfiler").then(mod => mod.LineProfilerView), + { + ssr: false, + loading: () => , + }, +) import { useViewMode } from "@/app/app/ViewModeContext" import { toast } from "sonner" diff --git a/js/cf-webapp/src/app/(dashboard)/review-optimizations/__tests__/action.test.ts b/js/cf-webapp/src/app/(dashboard)/review-optimizations/__tests__/action.test.ts new file mode 100644 index 000000000..5c94f1f96 --- /dev/null +++ b/js/cf-webapp/src/app/(dashboard)/review-optimizations/__tests__/action.test.ts @@ -0,0 +1,312 @@ +import { describe, it, expect, vi, beforeEach } from "vitest" +import { prisma, buildOptimizationOrCondition } from "@codeflash-ai/common" +import { getRepositoriesForAccountCached } from "@/lib/services/repository-utils" + +vi.mock("@/lib/server-action-timing", () => ({ + withTiming: vi.fn((_name: string, fn: Function) => fn), +})) + +vi.mock("@/lib/services/repository-utils", () => ({ + getRepositoriesForAccountCached: vi.fn(), +})) + +const mockPayload = { userId: "user-1", username: "testuser" } +const mockRepoIds = ["repo-1", "repo-2"] + +const mockEvents = [ + { + id: "evt-1", + trace_id: "trace-1", + function_name: "calculate", + file_path: "src/utils.py", + repository_id: "repo-1", + status: "approved", + is_staging: true, + created_at: new Date("2024-06-01"), + repository: { id: "repo-1", full_name: "org/repo", name: "repo" }, + }, + { + id: "evt-2", + trace_id: "trace-2", + function_name: "process", + file_path: "src/main.py", + repository_id: "repo-2", + status: "pending", + is_staging: true, + created_at: new Date("2024-06-02"), + repository: { id: "repo-2", full_name: "org/repo2", name: "repo2" }, + }, +] + +const mockFeatures = [ + { + trace_id: "trace-1", + review_quality: "high", + review_explanation: "Great optimization", + }, +] + +describe("getAllOptimizationEvents", () => { + let getAllOptimizationEvents: typeof import("../action").getAllOptimizationEvents + + beforeEach(async () => { + vi.mocked(getRepositoriesForAccountCached).mockResolvedValue({ + repoIds: mockRepoIds, + repos: [], + } as any) + vi.mocked(buildOptimizationOrCondition).mockReturnValue({}) + + const mod = await import("../action") + getAllOptimizationEvents = mod.getAllOptimizationEvents + }) + + describe("Path B: standard Prisma query", () => { + it("calls findMany and count in parallel", async () => { + vi.mocked(prisma.optimization_events.findMany).mockResolvedValue(mockEvents as any) + vi.mocked(prisma.optimization_events.count).mockResolvedValue(2) + vi.mocked(prisma.optimization_features.findMany).mockResolvedValue([]) + + await getAllOptimizationEvents({ payload: mockPayload as any }) + + expect(prisma.optimization_events.findMany).toHaveBeenCalledTimes(1) + expect(prisma.optimization_events.count).toHaveBeenCalledTimes(1) + }) + + it("batch-fetches optimization_features by trace_id array (not N+1)", async () => { + vi.mocked(prisma.optimization_events.findMany).mockResolvedValue(mockEvents as any) + vi.mocked(prisma.optimization_events.count).mockResolvedValue(2) + vi.mocked(prisma.optimization_features.findMany).mockResolvedValue(mockFeatures as any) + + await getAllOptimizationEvents({ payload: mockPayload as any }) + + // Single batch query with all trace IDs — NOT one per event + expect(prisma.optimization_features.findMany).toHaveBeenCalledTimes(1) + expect(prisma.optimization_features.findMany).toHaveBeenCalledWith({ + where: { trace_id: { in: ["trace-1", "trace-2"] } }, + select: { + trace_id: true, + review_quality: true, + review_explanation: true, + }, + }) + }) + + it("merges review_quality into events", async () => { + vi.mocked(prisma.optimization_events.findMany).mockResolvedValue(mockEvents as any) + vi.mocked(prisma.optimization_events.count).mockResolvedValue(2) + vi.mocked(prisma.optimization_features.findMany).mockResolvedValue(mockFeatures as any) + + const result = await getAllOptimizationEvents({ payload: mockPayload as any }) + + expect(result.events[0].review_quality).toBe("high") + expect(result.events[0].review_explanation).toBe("Great optimization") + expect(result.events[1].review_quality).toBeNull() + }) + + it("returns totalCount from count query", async () => { + vi.mocked(prisma.optimization_events.findMany).mockResolvedValue([]) + vi.mocked(prisma.optimization_events.count).mockResolvedValue(42) + vi.mocked(prisma.optimization_features.findMany).mockResolvedValue([]) + + const result = await getAllOptimizationEvents({ payload: mockPayload as any }) + expect(result.totalCount).toBe(42) + }) + + it("applies pagination with skip and take", async () => { + vi.mocked(prisma.optimization_events.findMany).mockResolvedValue([]) + vi.mocked(prisma.optimization_events.count).mockResolvedValue(0) + vi.mocked(prisma.optimization_features.findMany).mockResolvedValue([]) + + await getAllOptimizationEvents({ + payload: mockPayload as any, + page: 3, + pageSize: 25, + }) + + expect(prisma.optimization_events.findMany).toHaveBeenCalledWith( + expect.objectContaining({ + skip: 50, // (3 - 1) * 25 + take: 25, + }), + ) + }) + + it("uses default sort (created_at desc) when no sort provided", async () => { + vi.mocked(prisma.optimization_events.findMany).mockResolvedValue([]) + vi.mocked(prisma.optimization_events.count).mockResolvedValue(0) + vi.mocked(prisma.optimization_features.findMany).mockResolvedValue([]) + + await getAllOptimizationEvents({ payload: mockPayload as any }) + + expect(prisma.optimization_events.findMany).toHaveBeenCalledWith( + expect.objectContaining({ + orderBy: { created_at: "desc" }, + }), + ) + }) + + it("applies search filter", async () => { + vi.mocked(prisma.optimization_events.findMany).mockResolvedValue([]) + vi.mocked(prisma.optimization_events.count).mockResolvedValue(0) + vi.mocked(prisma.optimization_features.findMany).mockResolvedValue([]) + + await getAllOptimizationEvents({ + payload: mockPayload as any, + search: "calc", + }) + + const callArgs = vi.mocked(prisma.optimization_events.findMany).mock.calls[0][0] as any + const andClause = callArgs.where.AND + expect(andClause).toBeDefined() + expect(andClause.length).toBeGreaterThan(0) + + // Search should include OR across function_name, file_path, repository.full_name + const orClause = andClause.find((c: any) => c.OR)?.OR + expect(orClause).toHaveLength(3) + expect(orClause[0]).toEqual({ + function_name: { contains: "calc", mode: "insensitive" }, + }) + }) + + it("applies repository_id filter", async () => { + vi.mocked(prisma.optimization_events.findMany).mockResolvedValue([]) + vi.mocked(prisma.optimization_events.count).mockResolvedValue(0) + vi.mocked(prisma.optimization_features.findMany).mockResolvedValue([]) + + await getAllOptimizationEvents({ + payload: mockPayload as any, + filter: { repository_id: "repo-1" }, + }) + + const callArgs = vi.mocked(prisma.optimization_events.findMany).mock.calls[0][0] as any + const andClause = callArgs.where.AND + expect(andClause).toBeDefined() + expect(andClause).toContainEqual({ repository_id: "repo-1" }) + }) + }) + + describe("Path A: raw SQL query (review_quality sort/filter)", () => { + it("triggers when sort includes review_quality", async () => { + vi.mocked(prisma.$queryRawUnsafe) + .mockResolvedValueOnce([]) // events + .mockResolvedValueOnce([{ count: BigInt(0) }]) // count + + await getAllOptimizationEvents({ + payload: mockPayload as any, + sort: { review_quality: "desc" }, + }) + + expect(prisma.$queryRawUnsafe).toHaveBeenCalledTimes(2) + // Should NOT use standard Prisma findMany + expect(prisma.optimization_events.findMany).not.toHaveBeenCalled() + }) + + it("triggers when filter includes review_quality", async () => { + vi.mocked(prisma.$queryRawUnsafe) + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([{ count: BigInt(0) }]) + + await getAllOptimizationEvents({ + payload: mockPayload as any, + filter: { review_quality: "high" }, + }) + + expect(prisma.$queryRawUnsafe).toHaveBeenCalledTimes(2) + }) + + it("returns correct totalCount from BigInt conversion", async () => { + vi.mocked(prisma.$queryRawUnsafe) + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([{ count: BigInt(99) }]) + + const result = await getAllOptimizationEvents({ + payload: mockPayload as any, + sort: { review_quality: "asc" }, + }) + + expect(result.totalCount).toBe(99) + }) + + it("maps JOIN results to include repository object", async () => { + const rawEvents = [ + { + id: "evt-1", + trace_id: "trace-1", + review_quality: "high", + review_explanation: "Good", + repo_full_name: "org/repo", + repo_name: "repo", + repo_id: "repo-1", + }, + ] + vi.mocked(prisma.$queryRawUnsafe) + .mockResolvedValueOnce(rawEvents) + .mockResolvedValueOnce([{ count: BigInt(1) }]) + + const result = await getAllOptimizationEvents({ + payload: mockPayload as any, + sort: { review_quality: "desc" }, + }) + + expect(result.events[0].repository).toEqual({ + id: "repo-1", + full_name: "org/repo", + name: "repo", + }) + }) + + it("sets repository to null when repo_id is missing", async () => { + const rawEvents = [ + { + id: "evt-1", + trace_id: "trace-1", + review_quality: null, + review_explanation: null, + repo_full_name: null, + repo_name: null, + repo_id: null, + }, + ] + vi.mocked(prisma.$queryRawUnsafe) + .mockResolvedValueOnce(rawEvents) + .mockResolvedValueOnce([{ count: BigInt(1) }]) + + const result = await getAllOptimizationEvents({ + payload: mockPayload as any, + sort: { review_quality: "desc" }, + }) + + expect(result.events[0].repository).toBeNull() + }) + + it("includes LEFT JOIN in raw SQL queries", async () => { + vi.mocked(prisma.$queryRawUnsafe) + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([{ count: BigInt(0) }]) + + await getAllOptimizationEvents({ + payload: mockPayload as any, + sort: { review_quality: "desc" }, + }) + + const sql = vi.mocked(prisma.$queryRawUnsafe).mock.calls[0][0] as string + expect(sql).toContain("LEFT JOIN optimization_features") + expect(sql).toContain("LEFT JOIN repositories") + }) + }) + + describe("edge cases", () => { + it("handles empty repoIds", async () => { + vi.mocked(getRepositoriesForAccountCached).mockResolvedValue({ + repoIds: [], + repos: [], + } as any) + vi.mocked(prisma.optimization_events.findMany).mockResolvedValue([]) + vi.mocked(prisma.optimization_events.count).mockResolvedValue(0) + vi.mocked(prisma.optimization_features.findMany).mockResolvedValue([]) + + const result = await getAllOptimizationEvents({ payload: mockPayload as any }) + expect(result.events).toEqual([]) + }) + }) +}) diff --git a/js/cf-webapp/src/app/(dashboard)/review-optimizations/action.ts b/js/cf-webapp/src/app/(dashboard)/review-optimizations/action.ts index db3df8f87..0df89e278 100644 --- a/js/cf-webapp/src/app/(dashboard)/review-optimizations/action.ts +++ b/js/cf-webapp/src/app/(dashboard)/review-optimizations/action.ts @@ -1,11 +1,12 @@ "use server" import { getRepositoriesForAccountCached } from "@/lib/services/repository-utils" +import { withTiming } from "@/lib/server-action-timing" import { AccountPayload, buildOptimizationOrCondition, prisma } from "@codeflash-ai/common" -export async function getRepositoriesWithStagingEvents( - payload: AccountPayload, -): Promise> { - const { repoIds, repos: allRepos } = await getRepositoriesForAccountCached(payload) +export const getRepositoriesWithStagingEvents = withTiming( + "getRepositoriesWithStagingEvents", + async (payload: AccountPayload): Promise> => { + const { repoIds, repos: allRepos } = await getRepositoriesForAccountCached(payload) if (repoIds.length === 0) { return [] @@ -29,23 +30,26 @@ export async function getRepositoriesWithStagingEvents( full_name: repo.full_name, })) .sort((a, b) => a.full_name.localeCompare(b.full_name)) -} + }, +) -export async function getAllOptimizationEvents({ - payload, - search, - filter, - sort, - page = 1, - pageSize = 10, -}: { - payload: AccountPayload - search?: string - filter?: Record - sort?: { [key: string]: "asc" | "desc" } - page?: number - pageSize?: number -}) { +export const getAllOptimizationEvents = withTiming( + "getAllOptimizationEvents", + async ({ + payload, + search, + filter, + sort, + page = 1, + pageSize = 10, + }: { + payload: AccountPayload + search?: string + filter?: Record + sort?: { [key: string]: "asc" | "desc" } + page?: number + pageSize?: number + }) => { const repoIds = (await getRepositoriesForAccountCached(payload)).repoIds const where: any = { @@ -168,83 +172,84 @@ export async function getAllOptimizationEvents({ orderByClauses.push("oe.created_at DESC") } const orderByClause = orderByClauses.join(", ") - const events = await prisma.$queryRawUnsafe( - ` - SELECT - oe.*, - of.review_quality, - of.review_explanation - FROM optimization_events oe - LEFT JOIN optimization_features of ON oe.trace_id = of.trace_id - LEFT JOIN repositories r ON oe.repository_id = r.id - WHERE ${whereClause} - ORDER BY ${orderByClause} - LIMIT $${paramIndex} OFFSET $${paramIndex + 1} - `, - ...params, - pageSize, - (page - 1) * pageSize, - ) - // Get total count - const countResult = await prisma.$queryRawUnsafe<[{ count: bigint }]>( - ` - SELECT COUNT(*) as count - FROM optimization_events oe - LEFT JOIN optimization_features of ON oe.trace_id = of.trace_id - LEFT JOIN repositories r ON oe.repository_id = r.id - WHERE ${whereClause} - `, - ...params, - ) + const [events, countResult] = await Promise.all([ + prisma.$queryRawUnsafe( + ` + SELECT + oe.*, + of.review_quality, + of.review_explanation, + r.full_name as repo_full_name, + r.name as repo_name, + r.id as repo_id + FROM optimization_events oe + LEFT JOIN optimization_features of ON oe.trace_id = of.trace_id + LEFT JOIN repositories r ON oe.repository_id = r.id + WHERE ${whereClause} + ORDER BY ${orderByClause} + LIMIT $${paramIndex} OFFSET $${paramIndex + 1} + `, + ...params, + pageSize, + (page - 1) * pageSize, + ), + prisma.$queryRawUnsafe<[{ count: bigint }]>( + ` + SELECT COUNT(*) as count + FROM optimization_events oe + LEFT JOIN optimization_features of ON oe.trace_id = of.trace_id + LEFT JOIN repositories r ON oe.repository_id = r.id + WHERE ${whereClause} + `, + ...params, + ), + ]) const totalCount = Number(countResult[0].count) - // Fetch repository data for the events - const eventsWithRepo = await Promise.all( - events.map(async event => { - if (event.repository_id) { - const repository = await prisma.repositories.findUnique({ - where: { id: event.repository_id }, - }) - return { ...event, repository } - } - return { ...event, repository: null } - }), - ) + // Repository data is already included from the JOIN + const eventsWithRepo = events.map(event => ({ + ...event, + repository: event.repo_id ? { id: event.repo_id, full_name: event.repo_full_name, name: event.repo_name } : null, + })) return { events: eventsWithRepo, totalCount } } else { // Standard Prisma query with native orderBy const orderBy = sort || { created_at: "desc" } - const events = await prisma.optimization_events.findMany({ - where, - orderBy, - skip: (page - 1) * pageSize, - take: pageSize, - include: { - repository: true, + const [events, totalCount] = await Promise.all([ + prisma.optimization_events.findMany({ + where, + orderBy, + skip: (page - 1) * pageSize, + take: pageSize, + include: { + repository: true, + }, + }), + prisma.optimization_events.count({ where }), + ]) + + // Batch-fetch review data for all events in a single query + const traceIds = events.map(e => e.trace_id) + const features = await prisma.optimization_features.findMany({ + where: { trace_id: { in: traceIds } }, + select: { + trace_id: true, + review_quality: true, + review_explanation: true, }, }) + const featuresMap = new Map(features.map(f => [f.trace_id, f])) - // Fetch review_quality and review_explanation for each event - const eventsWithReviewData = await Promise.all( - events.map(async event => { - const features = await prisma.optimization_features.findUnique({ - where: { trace_id: event.trace_id }, - select: { - review_quality: true, - review_explanation: true, - }, - }) - - return { - ...event, - review_quality: features?.review_quality || null, - review_explanation: features?.review_explanation || null, - } - }), - ) - - const totalCount = await prisma.optimization_events.count({ where }) + const eventsWithReviewData = events.map(event => { + const f = featuresMap.get(event.trace_id) + return { + ...event, + review_quality: f?.review_quality || null, + review_explanation: f?.review_explanation || null, + } + }) return { events: eventsWithReviewData, totalCount } } -} + }, +) diff --git a/js/cf-webapp/src/app/api/traces/[trace_id]/save-modified-code/route.ts b/js/cf-webapp/src/app/api/traces/[trace_id]/save-modified-code/route.ts index 5bb677a4b..d90900240 100644 --- a/js/cf-webapp/src/app/api/traces/[trace_id]/save-modified-code/route.ts +++ b/js/cf-webapp/src/app/api/traces/[trace_id]/save-modified-code/route.ts @@ -1,10 +1,8 @@ import { NextRequest, NextResponse } from "next/server" -import { PrismaClient } from "@prisma/client" - -const prisma = new PrismaClient() +import { prisma } from "@/lib/prisma" export async function POST(request: NextRequest, props: { params: Promise<{ trace_id: string }> }) { - const params = await props.params; + const params = await props.params try { const { trace_id } = params const body = await request.json() diff --git a/js/cf-webapp/src/app/membench/page.tsx b/js/cf-webapp/src/app/membench/page.tsx new file mode 100644 index 000000000..6f3ea0a31 --- /dev/null +++ b/js/cf-webapp/src/app/membench/page.tsx @@ -0,0 +1,757 @@ +import { canAccessMembench } from "@/app/utils/auth" +import { redirect } from "next/navigation" +import { MembenchToggle } from "@/components/membench/membench-toggle" +import { + PeakMemoryChart, + AllocatorChart, + HeadroomChart, + MaxAllocChart, +} from "@/components/membench/membench-charts" + +export const metadata = { title: "Memory Benchmark — Unstructured" } + +/* ── static data ────────────────────────────────────────────────────── */ + +const SUITE = { + baseline: { + peak_gb: 1.66, + total_gb: 16.398, + allocs: 5_585_979, + wall_s: 76.0, + max_alloc_mb: 268, + tests: 18, + passed: 13, + failed: 5, + }, + current: { + peak_gb: 1.473, + total_gb: 20.239, + allocs: 6_210_809, + wall_s: 86.0, + max_alloc_mb: 134, + tests: 18, + passed: 13, + failed: 5, + }, +} + +const PEAK_DELTA_PCT = + ((SUITE.current.peak_gb - SUITE.baseline.peak_gb) / SUITE.baseline.peak_gb) * 100 +const PEAK_DELTA_MB = Math.abs((SUITE.current.peak_gb - SUITE.baseline.peak_gb) * 1024) +const MAX_ALLOC_DELTA_PCT = + ((SUITE.current.max_alloc_mb - SUITE.baseline.max_alloc_mb) / SUITE.baseline.max_alloc_mb) * 100 +const POD_RAM_LIMIT_GB = 32 + +const TOP_ALLOC_BASELINE: [string, number][] = [ + ["_create_inference_session", 1.386], + ["PIL Image.tobytes", 1.188], + ["PIL Image.new", 1.001], + ["load_prepare", 0.751], + ["render", 0.649], +] + +const TOP_ALLOC_CURRENT: [string, number][] = [ + ["PIL Image.tobytes", 1.802], + ["PIL Image.new", 1.556], + ["_create_inference_session", 1.328], + ["load_prepare", 1.172], + ["PIL Image.tobytes (2)", 0.889], +] + +const SCENARIO_TABLE = [ + { + scenario: "Full Suite (18 common tests)", + bl_peak: "1.660 GB", + cu_peak: "1.473 GB", + delta: "-11.3%", + bl_time: "76.0s", + cu_time: "86.0s", + }, + { + scenario: "API hi_res (layout-parser-paper, 16p)", + bl_peak: "1.515 GB", + cu_peak: "1.419 GB", + delta: "-6.3%", + bl_time: "53.6s", + cu_time: "60.2s", + }, + { + scenario: "od_only (Seeda Case Study)", + bl_peak: "1.127 GB", + cu_peak: "1.046 GB", + delta: "-7.2%", + bl_time: "1.96s", + cu_time: "2.13s", + }, +] + +const ENV_TABLE = [ + ["VM", "Azure Standard_D8s_v5 (8 vCPU, 32 GB RAM)"], + ["OS", "Ubuntu 20.04"], + ["Python", "3.12"], + ["Profiler", "memray --native (captures C/C++ malloc, mmap)"], + ["Test Runner", "memray run --native -o {out}.bin --force -m pytest -v"], + ["Baseline Env", "/home/krrt7/bench/baseline-core + baseline-env (pre-Feb 2026)"], + ["Current Env", "/home/krrt7/bench/current-core + current-env (main)"], + ["Pre-run Protocol", "VM reboot + 5-min idle wait (clean Azure telemetry window)"], + ["Production Target", "Knative pods, 1 CPU / 32 GB RAM, Standard_D48s_v5 nodes"], + ["Test Scope", "18 common partition tests (od_only, hi_res, pptx, docx)"], +] + +/* ── page ───────────────────────────────────────────────────────────── */ + +export default async function MembenchPage() { + const allowed = await canAccessMembench() + if (!allowed) redirect("/") + + const b = SUITE.baseline + const c = SUITE.current + + return ( +
+ {/* ── Hero ── */} +
+
+ UNSTRUCTURED +
+

+ Core Product Memory Benchmark +

+

+ Peak RAM reduction measured with memray --native across the partition test suite +

+
+ April 2026 + | + Baseline: pre-Feb 2026 + | + 18 common partition tests + | + Azure Standard_D8s_v5 VM +
+
+ + {/* ── Hero Metrics ── */} +
+ {[ + { + value: `${PEAK_DELTA_PCT.toFixed(1)}%`, + label: "Peak RAM", + detail: `${b.peak_gb.toFixed(2)} GB → ${c.peak_gb.toFixed(2)} GB`, + }, + { + value: `${Math.round(PEAK_DELTA_MB)} MB`, + label: "Absolute Reduction", + detail: "Peak high-water mark savings", + }, + { + value: `${MAX_ALLOC_DELTA_PCT.toFixed(0)}%`, + label: "Max Single Allocation", + detail: `${b.max_alloc_mb} MB → ${c.max_alloc_mb} MB`, + }, + { + value: "0", + label: "New Regressions", + detail: `Same ${c.passed}/${c.tests} pass rate on both`, + }, + ].map(m => ( +
+
+ {m.value} +
+
+ {m.label} +
+
{m.detail}
+
+ ))} +
+ + {/* ── Toggle + Views ── */} +
+ } engView={} /> +
+ + {/* ── Footer ── */} +
+
+ UNSTRUCTURED +
+

+ Core Product Memory Benchmark — April 2026 +

+
+
+ ) +} + +/* ═══════════════════════════════════════════════════════════════════════ + EXECUTIVE VIEW + ═══════════════════════════════════════════════════════════════════════ */ + +function ExecView() { + const b = SUITE.baseline + const c = SUITE.current + + return ( +
+ {/* ── Peak Memory by Scenario ── */} +
+ + + +
+ + {/* ── What Does This Mean? ── */} +
+
+ +

Lower OOM risk

+

+ Peak memory during the full partition suite dropped from {b.peak_gb.toFixed(2)} GB to{" "} + {c.peak_gb.toFixed(2)} GB — a {Math.round(PEAK_DELTA_MB)} MB reduction. For Knative + pods with a 32 GB RAM limit, this means more headroom before the OOM killer terminates + the container. +

+
+ +

+ Halved largest allocation +

+

+ The single largest memory allocation dropped from {b.max_alloc_mb} MB to{" "} + {c.max_alloc_mb} MB — a 50% reduction. Large contiguous allocations are the primary + cause of memory fragmentation and allocation failures even when total free memory + appears sufficient. +

+
+ +

+ Zero regressions +

+

+ Both environments pass the same {c.passed} of {c.tests} partition tests. The{" "} + {c.failed} failures are pre-existing docx edge cases present in the baseline — not + regressions from the optimization work. +

+
+
+
+ + {/* ── Pod Headroom ── */} +
+ + +
+
+
+ {((c.peak_gb / POD_RAM_LIMIT_GB) * 100).toFixed(1)}% +
+
+ of pod limit used (current) +
+
+
+
+ {(POD_RAM_LIMIT_GB - c.peak_gb).toFixed(1)} GB +
+
+ headroom remaining +
+
+
+

+ Note: Peak memory is measured per-process during document processing. Actual pod usage + includes OS overhead, model weights in shared memory, and other sidecar containers. + These figures represent the process-level high-water mark. +

+
+
+ + {/* ── Largest Single Allocation ── */} +
+ + + +
+ + {/* ── Suite-Level Comparison ── */} +
+ +
+ {/* header */} +
+
Metric
+
Baseline
+
Current
+
Delta
+
+ v.toFixed(3)} + better="lower" + /> + v.toFixed(1)} + better="lower" + /> + v.toLocaleString()} + better="lower" + /> + v.toFixed(0)} + better="lower" + /> + v.toFixed(1)} + better="lower" + /> + v.toFixed(0)} + better="higher" + /> +
+

+ Total allocated increased because current uses more frequent smaller allocations — peak + (the OOM-risk metric) still decreased. This pattern indicates better memory recycling. +

+
+
+ + {/* ── Implications & Next Steps ── */} +
+ + + + + + + + +
+
+ ) +} + +/* ═══════════════════════════════════════════════════════════════════════ + ENGINEERING VIEW + ═══════════════════════════════════════════════════════════════════════ */ + +function EngView() { + return ( +
+ {/* ── Per-Scenario Results ── */} +
+ +
+ + + + + + + + + + + + + {SCENARIO_TABLE.map((r, i) => ( + + + + + + + + + ))} + +
ScenarioBaseline PeakCurrent PeakDeltaBL TimeCU Time
{r.scenario} + {r.bl_peak} + + {r.cu_peak} + + {r.delta} + + {r.bl_time} + + {r.cu_time} +
+
+
+
+ + {/* ── Top Memory Allocators ── */} +
+ + + + +
+ +
+ BASELINE TOP 5 +
+ {TOP_ALLOC_BASELINE.map(([name, size], i) => ( +
+ + {i + 1}. + + + {name} + + + {size.toFixed(3)} GB + +
+ ))} +
+ +
+ CURRENT TOP 5 +
+ {TOP_ALLOC_CURRENT.map(([name, size], i) => ( +
+ + {i + 1}. + + + {name} + + + {size.toFixed(3)} GB + +
+ ))} +
+
+
+ + {/* ── Key Observations ── */} +
+ + + +
+ + {/* ── Benchmark Environment ── */} +
+ + + +
+ + {/* ── Methodology ── */} +
+ +
    +
  1. + VM rebooted before each environment's run to ensure clean memory state and enable + Azure telemetry correlation +
  2. +
  3. + 5-minute idle wait after reboot for OS caches, Azure agents, and background processes + to stabilize +
  4. +
  5. + Each test suite runs under memray run --native, which instruments both Python + allocations and native C/C++ allocations (malloc, calloc, realloc, mmap) via + LD_PRELOAD +
  6. +
  7. + memray stats extracts peak memory (high-water mark), total allocated, allocation + count, wall time, and top allocating functions from the binary trace +
  8. +
  9. + Common test set: 18 partition tests that exist in both baseline and current codebases, + ensuring apples-to-apples comparison +
  10. +
  11. + Identical test deselection applied to both: 6 baseline-only tests (docx/pptx edge + cases not present in current) excluded via pytest -k filters +
  12. +
+
+
+ + {/* ── Engineering Action Items ── */} +
+ + + + + + + + + +
+
+ ) +} + +/* ── shared components ──────────────────────────────────────────────── */ + +function Section({ + title, + subtitle, + children, +}: { + title: string + subtitle?: string + children: React.ReactNode +}) { + return ( +
+

+ {title} +

+ {subtitle && ( +

+ {subtitle} +

+ )} +
{children}
+
+ ) +} + +function Card({ children, className = "" }: { children: React.ReactNode; className?: string }) { + return ( +
+ {children} +
+ ) +} + +function ActionItem({ text, done = false }: { text: string; done?: boolean }) { + return ( +
+ + {done ? "●" : "○"} + + {text} +
+ ) +} + +function StatRow({ + label, + baseline, + current, + unit, + format, + better = "lower", +}: { + label: string + baseline: number + current: number + unit: string + format: (v: number) => string + better?: "lower" | "higher" +}) { + const delta = ((current - baseline) / baseline) * 100 + const improved = better === "lower" ? delta < 0 : delta > 0 + const deltaText = `${delta > 0 ? "+" : ""}${delta.toFixed(1)}%` + + return ( +
+
{label}
+
+ {format(baseline)} {unit} +
+
+ {format(current)} {unit} +
+
+ + {deltaText} + +
+
+ ) +} + +function ObservationCard({ + title, + badge, + badgeColor, + borderColor, + body, +}: { + title: string + badge: string + badgeColor: string + borderColor: string + body: string +}) { + return ( +
+
+ {title} + + {badge} + +
+

{body}

+
+ ) +} + +function DataTable({ columns, rows }: { columns: string[]; rows: string[][] }) { + return ( +
+ + + + {columns.map(c => ( + + ))} + + + + {rows.map((row, i) => ( + + {row.map((cell, j) => ( + + ))} + + ))} + +
+ {c} +
+ {cell} +
+
+ ) +} diff --git a/js/cf-webapp/src/app/observability/components/code-highlighter.tsx b/js/cf-webapp/src/app/observability/components/code-highlighter.tsx index 6d23e6019..82c8f9c4e 100644 --- a/js/cf-webapp/src/app/observability/components/code-highlighter.tsx +++ b/js/cf-webapp/src/app/observability/components/code-highlighter.tsx @@ -1,98 +1,84 @@ "use client" -import dynamic from "next/dynamic" +import { SyntaxHighlighter } from "@/lib/syntax-highlighter" import { memo } from "react" -const SyntaxHighlighter = dynamic( - () => import("react-syntax-highlighter").then(m => m.Prism), - { - ssr: false, - loading: () => ( -
-
-
-
-
- ), - } -) - export const zincDarkTheme = { 'code[class*="language-"]': { - color: 'rgb(250, 250, 250)', - background: 'none', - fontFamily: 'var(--font-mono)', - fontSize: '1em', - textAlign: 'left', - whiteSpace: 'pre', - wordSpacing: 'normal', - wordBreak: 'normal', - wordWrap: 'normal', - lineHeight: '1.5', + color: "rgb(250, 250, 250)", + background: "none", + fontFamily: "var(--font-mono)", + fontSize: "1em", + textAlign: "left", + whiteSpace: "pre", + wordSpacing: "normal", + wordBreak: "normal", + wordWrap: "normal", + lineHeight: "1.5", tabSize: 4, - hyphens: 'none', + hyphens: "none", }, 'pre[class*="language-"]': { - color: 'rgb(250, 250, 250)', - background: 'rgb(24, 24, 27)', - fontFamily: 'var(--font-mono)', - fontSize: '1em', - textAlign: 'left', - whiteSpace: 'pre', - wordSpacing: 'normal', - wordBreak: 'normal', - wordWrap: 'normal', - lineHeight: '1.5', + color: "rgb(250, 250, 250)", + background: "rgb(24, 24, 27)", + fontFamily: "var(--font-mono)", + fontSize: "1em", + textAlign: "left", + whiteSpace: "pre", + wordSpacing: "normal", + wordBreak: "normal", + wordWrap: "normal", + lineHeight: "1.5", tabSize: 4, - hyphens: 'none', - padding: '1em', - margin: '0', - overflow: 'auto', + hyphens: "none", + padding: "1em", + margin: "0", + overflow: "auto", }, comment: { - color: 'rgb(113, 113, 122)', - fontStyle: 'italic', + color: "rgb(113, 113, 122)", + fontStyle: "italic", }, - prolog: { color: 'rgb(113, 113, 122)' }, - doctype: { color: 'rgb(113, 113, 122)' }, - cdata: { color: 'rgb(113, 113, 122)' }, - keyword: { color: 'rgb(96, 165, 250)' }, - 'control-flow': { color: 'rgb(96, 165, 250)' }, - string: { color: 'rgb(134, 239, 172)' }, - 'attr-value': { color: 'rgb(134, 239, 172)' }, - function: { color: 'rgb(253, 224, 71)' }, - 'class-name': { color: 'rgb(253, 224, 71)' }, - number: { color: 'rgb(251, 146, 60)' }, - boolean: { color: 'rgb(251, 146, 60)' }, - operator: { color: 'rgb(161, 161, 170)' }, - punctuation: { color: 'rgb(161, 161, 170)' }, - variable: { color: 'rgb(250, 250, 250)' }, - property: { color: 'rgb(250, 250, 250)' }, - tag: { color: 'rgb(96, 165, 250)' }, - 'attr-name': { color: 'rgb(250, 250, 250)' }, + prolog: { color: "rgb(113, 113, 122)" }, + doctype: { color: "rgb(113, 113, 122)" }, + cdata: { color: "rgb(113, 113, 122)" }, + keyword: { color: "rgb(96, 165, 250)" }, + "control-flow": { color: "rgb(96, 165, 250)" }, + string: { color: "rgb(134, 239, 172)" }, + "attr-value": { color: "rgb(134, 239, 172)" }, + function: { color: "rgb(253, 224, 71)" }, + "class-name": { color: "rgb(253, 224, 71)" }, + number: { color: "rgb(251, 146, 60)" }, + boolean: { color: "rgb(251, 146, 60)" }, + operator: { color: "rgb(161, 161, 170)" }, + punctuation: { color: "rgb(161, 161, 170)" }, + variable: { color: "rgb(250, 250, 250)" }, + property: { color: "rgb(250, 250, 250)" }, + tag: { color: "rgb(96, 165, 250)" }, + "attr-name": { color: "rgb(250, 250, 250)" }, namespace: { opacity: 0.7 }, - selector: { color: 'rgb(253, 224, 71)' }, + selector: { color: "rgb(253, 224, 71)" }, important: { - color: 'rgb(251, 146, 60)', - fontWeight: 'bold', + color: "rgb(251, 146, 60)", + fontWeight: "bold", }, - atrule: { color: 'rgb(96, 165, 250)' }, - builtin: { color: 'rgb(253, 224, 71)' }, + atrule: { color: "rgb(96, 165, 250)" }, + builtin: { color: "rgb(253, 224, 71)" }, entity: { - color: 'rgb(250, 250, 250)', - cursor: 'help', + color: "rgb(250, 250, 250)", + cursor: "help", }, url: { - color: 'rgb(96, 165, 250)', - textDecoration: 'underline', + color: "rgb(96, 165, 250)", + textDecoration: "underline", }, inserted: { - color: 'rgb(134, 239, 172)', - background: 'rgba(134, 239, 172, 0.1)', + color: "rgb(134, 239, 172)", + background: "rgba(134, 239, 172, 0.1)", }, deleted: { - color: 'rgb(248, 113, 113)', - background: 'rgba(248, 113, 113, 0.1)', + color: "rgb(248, 113, 113)", + background: "rgba(248, 113, 113, 0.1)", }, } as const @@ -101,7 +87,7 @@ export const CODE_STYLE = { padding: "1rem", fontSize: "0.875rem", lineHeight: 1.5, - background: 'rgb(24, 24, 27)', + background: "rgb(24, 24, 27)", } as const export const CODE_STYLE_RELAXED = { @@ -109,7 +95,7 @@ export const CODE_STYLE_RELAXED = { padding: "1rem", fontSize: "0.875rem", lineHeight: 1.6, - background: 'rgb(24, 24, 27)', + background: "rgb(24, 24, 27)", } as const export const CODE_STYLE_SMALL = { @@ -117,7 +103,7 @@ export const CODE_STYLE_SMALL = { padding: "1rem", fontSize: "0.8125rem", lineHeight: 1.5, - background: 'rgb(24, 24, 27)', + background: "rgb(24, 24, 27)", } as const interface CodeHighlighterProps { @@ -129,13 +115,13 @@ interface CodeHighlighterProps { } const highlightStyle = { - backgroundColor: 'rgba(250, 204, 21, 0.15)', - display: 'block', - marginLeft: '-1rem', - marginRight: '-1rem', - paddingLeft: '1rem', - paddingRight: '1rem', - borderLeft: '3px solid rgb(250, 204, 21)', + backgroundColor: "rgba(250, 204, 21, 0.15)", + display: "block", + marginLeft: "-1rem", + marginRight: "-1rem", + paddingLeft: "1rem", + paddingRight: "1rem", + borderLeft: "3px solid rgb(250, 204, 21)", } export const CodeHighlighter = memo(function CodeHighlighter({ @@ -152,8 +138,8 @@ export const CodeHighlighter = memo(function CodeHighlighter({ return (lineNumber: number) => { const isHighlighted = highlightSet.has(lineNumber) return { - style: isHighlighted ? highlightStyle : { display: 'block' }, - 'data-highlighted': isHighlighted ? 'true' : undefined, + style: isHighlighted ? highlightStyle : { display: "block" }, + "data-highlighted": isHighlighted ? "true" : undefined, } } } @@ -173,4 +159,4 @@ export const CodeHighlighter = memo(function CodeHighlighter({ {code} ) -}) \ No newline at end of file +}) diff --git a/js/cf-webapp/src/app/observability/llm-call/[id]/page.tsx b/js/cf-webapp/src/app/observability/llm-call/[id]/page.tsx index d6f625063..5d03168bf 100644 --- a/js/cf-webapp/src/app/observability/llm-call/[id]/page.tsx +++ b/js/cf-webapp/src/app/observability/llm-call/[id]/page.tsx @@ -31,21 +31,21 @@ export async function generateMetadata(props: LLMCallDetailPageProps): Promise {/* Header */} diff --git a/js/cf-webapp/src/app/report/page.tsx b/js/cf-webapp/src/app/report/page.tsx new file mode 100644 index 000000000..fea83c482 --- /dev/null +++ b/js/cf-webapp/src/app/report/page.tsx @@ -0,0 +1,993 @@ +import { isTeamMember } from "@/app/utils/auth" +import { redirect } from "next/navigation" +import Image from "next/image" +import { ReportToggle } from "@/components/report/report-toggle" +import { + LatencyChart, + BundleChart, + OnboardingChart, + CategoryPie, +} from "@/components/report/report-charts" + +export const metadata = { title: "Performance Report — Codeflash" } + +/* ── static data ────────────────────────────────────────────────────── */ + +const PAGE_RESULTS: { page: string; before: number | null; after: number | null; note?: string }[] = + [ + { page: "Onboarding", before: 10275, after: 8108 }, + { page: "API Keys", before: 6415, after: 5989 }, + { page: "Billing", before: 4108, after: 4383 }, + { page: "Traces", before: 3353, after: 3888 }, + { page: "Getting Started", before: 4513, after: 5350 }, + { page: "Dashboard", before: 16816, after: 30000, note: "timeout — bug fixed separately" }, + { + page: "Review Optimizations", + before: 4376, + after: 16735, + note: "only 3 samples, low confidence", + }, + ] + +const FULL_PAGE_TABLE: { + route: string + avgBefore: number | null + avgAfter: number | null + p95Before: number | null + p95After: number | null +}[] = [ + { route: "/dashboard", avgBefore: 16816, avgAfter: 30000, p95Before: 30000, p95After: 30000 }, + { route: "/onboarding", avgBefore: 10275, avgAfter: 8108, p95Before: 20092, p95After: 8108 }, + { route: "/apikeys", avgBefore: 6415, avgAfter: 5989, p95Before: 11120, p95After: 10291 }, + { route: "/trace/:trace_id", avgBefore: 6909, avgAfter: null, p95Before: 9013, p95After: null }, + { route: "/login", avgBefore: 4958, avgAfter: null, p95Before: 7647, p95After: null }, + { route: "/repositories", avgBefore: 4306, avgAfter: null, p95Before: 7110, p95After: null }, + { + route: "/review-optimizations", + avgBefore: 4376, + avgAfter: 16735, + p95Before: 4985, + p95After: 16735, + }, + { route: "/getting-started", avgBefore: 4513, avgAfter: 5350, p95Before: 4513, p95After: 5588 }, + { route: "/repositories/:id", avgBefore: 5292, avgAfter: null, p95Before: 5292, p95After: null }, + { route: "/billing", avgBefore: 4108, avgAfter: 4383, p95Before: 4108, p95After: 4383 }, + { + route: "/observability/traces", + avgBefore: 3353, + avgAfter: 3888, + p95Before: 3981, + p95After: 3888, + }, + { route: "/codeflash/auth", avgBefore: 1093, avgAfter: 3465, p95Before: 1673, p95After: 5079 }, + { route: "/members", avgBefore: 1658, avgAfter: null, p95Before: 1658, p95After: null }, +] + +const BUNDLE_TABLE = [ + { opt: "Sentry Replay lazy-load", savings: "~600 KB deferred", pr: "#2554" }, + { opt: "prism-react-renderer dynamic import", savings: "~132 KB", pr: "#2557" }, + { opt: "framer-motion → motion/react", savings: "~70 KB", pr: "#2556" }, + { opt: "PrismLight switch", savings: "~50 KB", pr: "#2539" }, + { + opt: "Unused dep removal (github-markdown-css, react-papaparse, @azure/msal-node)", + savings: "variable", + pr: "#2562", + }, + { opt: "@sentry/node → @sentry/nextjs", savings: "duplicate SDK eliminated", pr: "#2555" }, +] + +const RUNTIME_TABLE = [ + { opt: "N+1 query elimination", impact: "1 query vs N for optimization_features", pr: "#2544" }, + { + opt: "Promise.all parallelization", + impact: "Concurrent DB fetches on 5 pages", + pr: "#2545, #2546, #2559, #2561, #2560", + }, + { opt: "React cache() dedup", impact: "2 identical Prisma calls → 1 per request", pr: "#2560" }, + { opt: "PostHog singleton", impact: "Eliminated repeated HTTP client creation", pr: "#2558" }, + { + opt: "PrismaClient singleton", + impact: "Consolidated per-file PrismaClient instances", + pr: "#2543", + }, +] + +const OBSERVABILITY_TABLE = [ + { + name: "OTel + Sentry bridge", + detail: "SentrySpanProcessor, SentryPropagator, SentrySampler", + pr: "#2547", + }, + { name: "Server action timing", detail: "withTiming() wrapper with Sentry spans", pr: "#2552" }, + { name: "Slow query logging", detail: ">500ms queries forwarded to Sentry", pr: "#2547" }, + { + name: "PostHog analytics", + detail: "trackOptimizationReviewed + key user actions", + pr: "#2552", + }, + { name: "Bundle analysis", detail: "Route size analysis scripts", pr: "#2553" }, +] + +type PRCategory = + | "Bundle size" + | "Runtime" + | "Observability" + | "Testing" + | "Cleanup" + | "CI" + | "Bugfix" +const CATEGORY_COLORS: Record = { + "Bundle size": "text-amber-600 dark:text-amber-400", + Runtime: "text-green-600 dark:text-green-400", + Observability: "text-amber-700 dark:text-amber-500", + Testing: "text-purple-600 dark:text-purple-400", + Cleanup: "text-zinc-500 dark:text-zinc-400", + CI: "text-pink-600 dark:text-pink-400", + Bugfix: "text-red-600 dark:text-red-400", +} + +const PR_INVENTORY: { num: string; pr: string; title: string; cat: PRCategory }[] = [ + { num: "1", pr: "#2539", title: "PrismLight switch", cat: "Bundle size" }, + { num: "2", pr: "#2540", title: "Named diff + Sentry import", cat: "Bundle size" }, + { num: "3", pr: "#2543", title: "PrismaClient singleton", cat: "Runtime" }, + { num: "4", pr: "#2544", title: "N+1 query elimination", cat: "Runtime" }, + { num: "5", pr: "#2545", title: "Members page parallel fetch", cat: "Runtime" }, + { num: "6", pr: "#2546", title: "Repository page parallel fetch", cat: "Runtime" }, + { num: "7", pr: "#2547", title: "Observability stack (OTel + Sentry)", cat: "Observability" }, + { num: "8", pr: "#2552", title: "Server action timing + analytics", cat: "Observability" }, + { num: "9", pr: "#2553", title: "Test coverage + bundle analysis", cat: "Testing" }, + { num: "10", pr: "#2554", title: "Sentry Replay lazy-load", cat: "Bundle size" }, + { num: "11", pr: "#2555", title: "@sentry/nextjs consistency", cat: "Bundle size" }, + { num: "12", pr: "#2556", title: "framer-motion → motion/react", cat: "Bundle size" }, + { num: "13", pr: "#2557", title: "LineProfilerView dynamic import", cat: "Bundle size" }, + { num: "14", pr: "#2558", title: "PostHog singleton", cat: "Runtime" }, + { num: "15", pr: "#2559", title: "Optimization event parallel fetch", cat: "Runtime" }, + { num: "16", pr: "#2560", title: "React cache() dedup", cat: "Runtime" }, + { num: "17", pr: "#2561", title: "LLM call detail parallel fetch", cat: "Runtime" }, + { num: "18", pr: "#2562", title: "Unused dep removal", cat: "Cleanup" }, + { num: "19", pr: "#2563", title: "Quality gates CI workflow", cat: "CI" }, + { num: "—", pr: "#2564", title: "Sidebar refetch loop fix", cat: "Bugfix" }, +] + +/* ── helpers ────────────────────────────────────────────────────────── */ + +function fmt(v: number | null) { + if (v === null) return "—" + return v.toLocaleString() +} + +function pctBadge(before: number | null, after: number | null) { + if (before === null || after === null) + return + const pct = ((after - before) / before) * 100 + const improved = pct < 0 + return ( + + {pct > 0 ? "+" : ""} + {pct.toFixed(0)}% + + ) +} + +/* ── page ───────────────────────────────────────────────────────────── */ + +export default async function ReportPage() { + const teamMember = await isTeamMember() + if (!teamMember) redirect("/") + + return ( +
+ {/* ── Hero ── */} +
+ Codeflash + Codeflash +

+ Web App Performance Optimization +

+

+ How we made the Codeflash dashboard faster, lighter, and more reliable +

+
+ April 4, 2026 + · + 20 changes shipped + · + Zero downtime +
+
+ + {/* ── Hero Metrics ── */} +
+ {[ + { + value: "-55%", + label: "Slowest Responses", + detail: "p95 latency: 1.2s → 0.6s", + color: "text-green-600 dark:text-green-400", + }, + { + value: "-25%", + label: "Typical Responses", + detail: "p75 latency: 289ms → 216ms", + color: "text-green-600 dark:text-green-400", + }, + { + value: "850 KB", + label: "Smaller Downloads", + detail: "Removed from every page load", + color: "text-amber-500 dark:text-[#ffd227]", + }, + { + value: "0", + label: "New Bugs", + detail: "All changes verified, zero errors", + color: "text-amber-500 dark:text-[#ffd227]", + }, + ].map(m => ( +
+
+ {m.value} +
+
+ {m.label} +
+
{m.detail}
+
+ ))} +
+ + {/* ── Toggle + Views ── */} +
+ } engView={} /> +
+ + {/* ── Footer ── */} +
+ Codeflash + Codeflash +

April 2026

+
+
+ ) +} + +/* ═══════════════════════════════════════════════════════════════════════ + EXECUTIVE VIEW + ═══════════════════════════════════════════════════════════════════════ */ + +function ExecView() { + return ( +
+ {/* ── Cost Impact ── */} +
+ +
+
+
+ $660 +
+
+ estimated annual savings +
+
+
+

+ The webapp currently runs on an Azure Premium V2 (P1v2) instance at $81/month. With + the performance optimizations — parallel queries, eliminated N+1 fetches, fixed + infinite request loop, and reduced CPU load per request — the app can be downscaled + to a Basic B2 instance. +

+
+
+
+ + {/* Infra comparison */} +
+
+
+ Current +
+
+ P1v2 — Premium V2 +
+
1 vCPU · 3.5 GB RAM
+
+
+ Monthly + $81 +
+
+ Annual + $972 +
+
+
+
+
+
+ Recommended +
+
+ B2 — Basic +
+
2 vCPU · 3.5 GB RAM
+
+
+ Monthly + $26 +
+
+ Annual + $312 +
+
+
+
+ + {/* Why + monitoring */} +
+ +

+ Why downscaling is safe +

+
    +
  • The webapp handles only ~14 transactions/day — minimal compute needs
  • +
  • The infinite request loop (hundreds of requests/sec) has been fixed
  • +
  • + Database queries now run in parallel, completing faster and releasing threads sooner +
  • +
  • N+1 query pattern eliminated — fewer round-trips to the database
  • +
  • B2 actually gives more CPU cores (2 vs 1) at a third of the price
  • +
+

+ Note: B-tier does not include deployment slots. If staging/production slot swapping is + required, Standard S1 ($73/mo, saving $96/year) is the minimum tier that supports it. +

+
+ +

+ Monitoring cost reduction +

+
+ 90% +
+
+ less Sentry event volume +
+

+ Trace sampling reduced from 100% to 10% in production. Same error visibility, same + alerting — just 90% fewer billable events. Exact dollar savings depend on your Sentry + plan tier. +

+
+
+ 1 pre-existing bug found & fixed +
+

+ Dashboard sidebar was generating hundreds of server requests per second. This silent + resource drain is now eliminated. +

+
+
+
+
+ + {/* ── What Does This Mean? ── */} +
+
+ {[ + { + title: "Users wait less", + body: "The slowest page loads improved by over half a second. Users who previously waited 1.2 seconds for the app to respond now wait 0.6 seconds.", + }, + { + title: "Pages load faster", + body: "We removed 850 KB of code that was being downloaded on every page visit. That's the equivalent of a high-resolution photo — gone from every load.", + }, + { + title: "Nothing broke", + body: "All 20 changes were individually tested and verified. Error monitoring confirms zero new issues were introduced.", + }, + ].map(c => ( + +

{c.title}

+

+ {c.body} +

+
+ ))} +
+
+ + {/* ── Response Time ── */} +
+ + + +
+ + {/* ── Onboarding spotlight ── */} +
+ + + +
+ + {/* ── Bundle Size ── */} +
+ + + +
+ + {/* ── What We Changed ── */} +
+
+
+ + {[ + { + icon: "⚡", + title: "Faster Database Queries", + desc: "Eliminated redundant queries and ran independent queries in parallel instead of one-by-one. 5 pages now load data concurrently.", + }, + { + icon: "📦", + title: "Smaller Page Downloads", + desc: "Removed unused code, deferred non-critical libraries, and switched to lighter alternatives. 850 KB+ removed from every initial page load.", + }, + { + icon: "📊", + title: "Better Monitoring", + desc: "Added performance tracking for every server action, slow query alerts, and user behavior analytics. We can now see exactly what's slow and why.", + }, + { + icon: "🧪", + title: "Automated Testing", + desc: "Added 39 tests covering critical paths. Set up a CI pipeline that checks code quality, runs tests, and reports page sizes on every change.", + }, + { + icon: "🔧", + title: "Bug Fix", + desc: "Found and fixed a dashboard bug where the sidebar was making hundreds of server requests per second due to an infinite loop. This was wasting resources silently.", + }, + ].map(item => ( +
+ {item.icon} +
+
+ {item.title} +
+
+ {item.desc} +
+
+
+ ))} +
+
+
+ +
+ Changes by Category +
+ +
+
+
+
+ + {/* ── Page-by-Page ── */} +
+ +
+
+
Page
+
Before
+
After
+
Change
+
+ {PAGE_RESULTS.map(r => ( +
+
+ + {r.page} + + {r.note && ( + {r.note} + )} +
+
+ {fmt(r.before)} ms +
+
+ {fmt(r.after)} ms +
+
{pctBadge(r.before, r.after)}
+
+ ))} +
+

+ Note: Many pages have limited post-optimization data because we reduced monitoring from + 100% to 10% sampling, and only ~2 hours had passed at measurement time. Full results + expected within 7 days. +

+
+
+ + {/* ── Recommended Actions ── */} +
+ + + + + + +
+
+ ) +} + +/* ═══════════════════════════════════════════════════════════════════════ + ENGINEERING VIEW + ═══════════════════════════════════════════════════════════════════════ */ + +function EngView() { + return ( +
+ {/* ── Span Latency ── */} +
+ + + +
+ + {/* ── Error Count ── */} +
+
+ +
+ Before (14d) +
+
+ 2 +
+
errors
+
+ +
+ After (post-merge) +
+
+ 0 +
+
+ errors — zero regressions +
+
+ +
+ Transactions (14d) +
+
+ 107 → 41 +
+
+ sampling reduced 100% → 10% +
+
+
+
+ + {/* ── Full Page Load Table ── */} +
+ +
+ + + + + + + + + + + + {FULL_PAGE_TABLE.map((r, i) => ( + + + + + + + + ))} + +
RouteAvg BeforeAvg Afterp95 Beforep95 After
+ {r.route} + + {fmt(r.avgBefore)} + + {fmt(r.avgAfter)} + + {fmt(r.p95Before)} + + {fmt(r.p95After)} +
+
+

+ — = no post-merge samples (10% sampling + short window). Dashboard 30s = sidebar refetch + loop timeout (fixed PR #2564). +

+
+
+ + {/* ── Bundle Size Details ── */} +
+ + [r.opt, r.savings, r.pr])} + /> + +
+ + {/* ── Runtime Details ── */} +
+ + [r.opt, r.impact, r.pr])} + /> + +
+ + {/* ── Observability Stack ── */} +
+ + [r.name, r.detail, r.pr])} + /> + +
+ + {/* ── Testing & CI ── */} +
+ +
    +
  • + 39 tests across 4 files: withTiming wrapper, N+1 fix verification, parallel fetches, + access control, error handling (PR #2553) +
  • +
  • + Quality gates CI workflow: type-check → tests → build → route size PR comment (PR + #2563) +
  • +
  • Trace sampling: 100% → 10% in production (PR #2547)
  • +
+
+
+ + {/* ── Full PR Inventory ── */} +
+ +
+ + + + + + + + + + + {PR_INVENTORY.map((r, i) => ( + + + + + + + ))} + +
#PRTitleCategory
{r.num}{r.pr}{r.title}{r.cat}
+
+
+
+ + {/* ── Pre-Existing Issues ── */} +
+ + + +
+ + {/* ── Infrastructure ── */} +
+ +
+
+
+ Current +
+
+ P1v2 (PremiumV2) +
+
+ 1 vCPU · 3.5 GB · 1 instance · $81/mo +
+
+
+
+
+ Recommended +
+
+ B2 (Basic) +
+
+ 2 vCPU · 3.5 GB · $26/mo (saves $660/yr) +
+
+
+

+ B-tier lacks deployment slots. If slot swapping is required: S1 ($73/mo, saves $96/yr). +

+
+
+ + {/* ── Engineering Action Items ── */} +
+ + + + + + + + + +
+
+ ) +} + +/* ── shared components ──────────────────────────────────────────────── */ + +function Section({ + title, + subtitle, + children, +}: { + title: string + subtitle?: string + children: React.ReactNode +}) { + return ( +
+

+ {title} +

+ {subtitle && ( +

+ {subtitle} +

+ )} +
{children}
+
+ ) +} + +function Card({ children, className = "" }: { children: React.ReactNode; className?: string }) { + return ( +
+ {children} +
+ ) +} + +function ActionItem({ text }: { text: string }) { + return ( +
+ + {text} +
+ ) +} + +function IssueCard({ + title, + severity, + borderColor, + badgeColor, + body, + fix, +}: { + title: string + severity: string + borderColor: string + badgeColor: string + body: string + fix?: string +}) { + return ( +
+
+ {title} + + {severity} + +
+

{body}

+ {fix && ( +

+ Fix: {fix} +

+ )} +
+ ) +} + +function DataTable({ columns, rows }: { columns: string[]; rows: string[][] }) { + return ( +
+ + + + {columns.map(c => ( + + ))} + + + + {rows.map((row, i) => ( + + {row.map((cell, j) => ( + + ))} + + ))} + +
+ {c} +
+ {cell} +
+
+ ) +} diff --git a/js/cf-webapp/src/app/trace/[trace_id]/page.tsx b/js/cf-webapp/src/app/trace/[trace_id]/page.tsx index 2aebd4fac..e62220612 100644 --- a/js/cf-webapp/src/app/trace/[trace_id]/page.tsx +++ b/js/cf-webapp/src/app/trace/[trace_id]/page.tsx @@ -1,4 +1,4 @@ -import { PrismaClient } from "@prisma/client" +import { cache } from "react" import { notFound } from "next/navigation" import Link from "next/link" import { ExperimentMetadata } from "@/lib/types" // Your defined types @@ -6,13 +6,29 @@ import MonacoDiffViewer from "@/components/trace/monaco-diff-viewer" import { Metadata } from "next" // For Next.js metadata API import { auth0 } from "@/lib/auth0" import { isTeamMember } from "@/app/utils/auth" +import { prisma } from "@/lib/prisma" interface TraceDetailsPageProps { params: Promise<{ trace_id: string }> } -const prisma = new PrismaClient() + +// Deduplicate the Prisma query between generateMetadata and the page component. +// React cache() ensures the same trace_id only hits the DB once per request. +const getOptimizationFeature = cache(async (trace_id: string) => { + return prisma.optimization_features.findUnique({ + where: { trace_id }, + select: { + experiment_metadata: true, + metadata: true, + organization: true, + repository: true, + review_quality: true, + review_explanation: true, + }, + }) +}) // Function to generate dynamic metadata (e.g., page title) export async function generateMetadata(props: TraceDetailsPageProps): Promise { const params = await props.params @@ -22,16 +38,7 @@ export async function generateMetadata(props: TraceDetailsPageProps): Promise> = null try { - optimizationFeature = await prisma.optimization_features.findUnique({ - where: { trace_id: trace_id }, - select: { - experiment_metadata: true, // Prisma handles JSONB parsing - metadata: true, // Include metadata field which stores modified code - organization: true, - repository: true, - review_quality: true, - review_explanation: true, - // Select other fields if needed by MonacoDiffViewer for its header/display - }, - }) + optimizationFeature = await getOptimizationFeature(trace_id) } catch (error) { console.error(`[TracePage] Failed to fetch data for trace_id ${trace_id}:`, error) - // Optionally, render a specific error UI component here instead of notFound() - // For now, notFound() will trigger the 404 page, which is reasonable if data fetch fails badly. - // Or you could pass an error state to MonacoDiffViewer to display. - // For this detailed guide, we assume MonacoDiffViewer will handle 'null' metadata. } // If feature is not found, or metadata is explicitly null (and you expect it for valid traces) diff --git a/js/cf-webapp/src/app/utils/auth.ts b/js/cf-webapp/src/app/utils/auth.ts index a3d592e57..a1346c6d5 100644 --- a/js/cf-webapp/src/app/utils/auth.ts +++ b/js/cf-webapp/src/app/utils/auth.ts @@ -2,7 +2,7 @@ import { auth0 } from "@/lib/auth0" import { cache } from "react" -import { isTeamMemberCheck } from "@/lib/team-members" +import { isTeamMemberCheck, isMembenchAllowed } from "@/lib/team-members" const getCachedSession = cache(async () => { return auth0.getSession() @@ -55,3 +55,9 @@ export const getAuthenticatedTeamSession = cache(async () => { return isTeamMemberCheck(session.user) ? session : null }) + +export async function canAccessMembench(): Promise { + const session = await getCachedSession() + if (!session?.user) return false + return isMembenchAllowed(session.user) +} diff --git a/js/cf-webapp/src/components/Editor/monaco-diff-editor-github.tsx b/js/cf-webapp/src/components/Editor/monaco-diff-editor-github.tsx index 49bfa77bd..7587b67c0 100644 --- a/js/cf-webapp/src/components/Editor/monaco-diff-editor-github.tsx +++ b/js/cf-webapp/src/components/Editor/monaco-diff-editor-github.tsx @@ -6,7 +6,7 @@ import { Editor, DiffEditor } from "@monaco-editor/react" import type { editor } from "monaco-editor" import ReactMarkdown from "react-markdown" import remarkGfm from "remark-gfm" -import * as Diff from "diff" +import { createPatch } from "diff" import { ChevronRight, FileText, @@ -383,7 +383,7 @@ const MonacoDiffEditorGithub: React.FC = ({ diff += `\n` Object.entries(modifiedContents).forEach(([filePath, content]) => { - const patch = Diff.createPatch( + const patch = createPatch( filePath, content.oldContent, content.newContent, diff --git a/js/cf-webapp/src/components/conditional-layout.tsx b/js/cf-webapp/src/components/conditional-layout.tsx index 76da927c3..6c4c3b115 100644 --- a/js/cf-webapp/src/components/conditional-layout.tsx +++ b/js/cf-webapp/src/components/conditional-layout.tsx @@ -22,12 +22,14 @@ export function ConditionalLayout({ const [isAnnouncementVisible, setIsAnnouncementVisible] = useState(true) const shouldHideLayout = - pathname !== null && ( - HIDDEN_PAGES.includes(pathname) || + pathname !== null && + (HIDDEN_PAGES.includes(pathname) || pathname.startsWith("/trace/") || pathname.startsWith("/observability") || - !user - ) + pathname.startsWith("/roadmap") || + pathname.startsWith("/report") || + pathname.startsWith("/membench") || + !user) // Auto-collapse announcement after 4 seconds useEffect(() => { diff --git a/js/cf-webapp/src/components/dashboard/sidebar.tsx b/js/cf-webapp/src/components/dashboard/sidebar.tsx index 8ce0a8928..23aa1ef48 100644 --- a/js/cf-webapp/src/components/dashboard/sidebar.tsx +++ b/js/cf-webapp/src/components/dashboard/sidebar.tsx @@ -106,8 +106,10 @@ export function Sidebar({ className, user, isLoading, error }: SidebarProps): JS if (subscriptionFetchRef.current) return subscriptionFetchRef.current = true + let cancelled = false getCurrentUserSubscriptionData() .then(data => { + if (cancelled) return if (data) { setSubscription({ optimizations_used: data.optimizations_used || 0, @@ -117,10 +119,14 @@ export function Sidebar({ className, user, isLoading, error }: SidebarProps): JS setSubscription(null) } }) - .catch(() => setSubscription(null)) - .finally(() => { - subscriptionFetchRef.current = false + .catch(() => { + if (!cancelled) setSubscription(null) }) + + return () => { + cancelled = true + subscriptionFetchRef.current = false + } }, [mode]) const toggleTheme = () => { diff --git a/js/cf-webapp/src/components/membench/membench-charts.tsx b/js/cf-webapp/src/components/membench/membench-charts.tsx new file mode 100644 index 000000000..fdbc2962b --- /dev/null +++ b/js/cf-webapp/src/components/membench/membench-charts.tsx @@ -0,0 +1,372 @@ +"use client" + +import { useMemo } from "react" +import { Bar } from "react-chartjs-2" +import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Tooltip, Legend } from "chart.js" +import ChartDataLabels from "chartjs-plugin-datalabels" + +ChartJS.register(CategoryScale, LinearScale, BarElement, Tooltip, Legend, ChartDataLabels) + +const GREEN = "#4ade80" +const LIGHT_GRAY = "#71717a" +const GRAY = "#a1a1aa" +const DARK = "#09090b" +const ZINC_700 = "#3f3f46" +const ZINC_400 = "#a1a1aa" + +function useIsDark() { + if (typeof window === "undefined") return true + return window.matchMedia("(prefers-color-scheme: dark)").matches +} + +function useChartOptions(isDark: boolean) { + return useMemo( + () => ({ + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: "top" as const, + labels: { + color: isDark ? ZINC_400 : "#52525b", + font: { family: "Inter, system-ui, sans-serif", size: 13 }, + }, + }, + datalabels: { + display: false as const, + }, + }, + scales: { + x: { + ticks: { color: isDark ? ZINC_400 : "#71717a", font: { size: 12 } }, + grid: { display: false }, + border: { display: false }, + }, + y: { + ticks: { color: isDark ? ZINC_400 : "#71717a", font: { size: 12 } }, + grid: { color: isDark ? ZINC_700 : "#e4e4e7" }, + border: { display: false }, + }, + }, + }), + [isDark], + ) +} + +export function PeakMemoryChart() { + const isDark = useIsDark() + const baseOptions = useChartOptions(isDark) + + const data = useMemo( + () => ({ + labels: [ + "Full Test Suite (18 tests)", + "API hi_res Pipeline (16p PDF)", + "od_only (Seeda Case Study)", + ], + datasets: [ + { + label: "Baseline (pre-Feb 2026)", + data: [1.66, 1.515, 1.127], + backgroundColor: LIGHT_GRAY, + borderRadius: 6, + datalabels: { + display: true, + anchor: "end" as const, + align: "top" as const, + formatter: (v: number) => `${v.toFixed(3)} GB`, + color: isDark ? GRAY : "#71717a", + font: { size: 13, family: "Inter, system-ui, sans-serif" }, + }, + }, + { + label: "Current (main)", + data: [1.473, 1.419, 1.046], + backgroundColor: GREEN, + borderRadius: 6, + datalabels: { + display: true, + anchor: "end" as const, + align: "top" as const, + formatter: (v: number) => `${v.toFixed(3)} GB`, + color: isDark ? GREEN : "#16a34a", + font: { size: 13, family: "Inter, system-ui, sans-serif" }, + }, + }, + ], + }), + [isDark], + ) + + return ( +
+ +
+ ) +} + +export function AllocatorChart() { + const isDark = useIsDark() + const baseOptions = useChartOptions(isDark) + + const data = useMemo( + () => ({ + labels: [ + "_create_inference_session", + "PIL Image.tobytes", + "PIL Image.new", + "load_prepare", + "render", + "PIL Image.tobytes (2)", + ], + datasets: [ + { + label: "Baseline", + data: [1.386, 1.188, 1.001, 0.751, 0.649, 0], + backgroundColor: LIGHT_GRAY, + borderRadius: 4, + datalabels: { + display: true, + anchor: "end" as const, + align: "right" as const, + formatter: (v: number) => `${v.toFixed(2)} GB`, + color: isDark ? GRAY : "#71717a", + font: { size: 12, family: "Inter, system-ui, sans-serif" }, + }, + }, + { + label: "Current", + data: [1.328, 1.802, 1.556, 1.172, 0, 0.889], + backgroundColor: GREEN, + borderRadius: 4, + datalabels: { + display: true, + anchor: "end" as const, + align: "right" as const, + formatter: (v: number) => `${v.toFixed(2)} GB`, + color: isDark ? GREEN : "#16a34a", + font: { size: 12, family: "Inter, system-ui, sans-serif" }, + }, + }, + ], + }), + [isDark], + ) + + return ( +
+ +
+ ) +} + +export function HeadroomChart() { + const isDark = useIsDark() + + const data = useMemo( + () => ({ + labels: ["Pod RAM"], + datasets: [ + { + label: "Pod Limit (32 GB)", + data: [32], + backgroundColor: isDark ? "rgba(39,39,42,0.5)" : "rgba(228,228,231,0.8)", + borderRadius: 6, + datalabels: { + display: true, + anchor: "center" as const, + align: "center" as const, + formatter: (): string => "32 GB limit", + color: isDark ? GRAY : "#71717a", + font: { size: 13, family: "Inter, system-ui, sans-serif" }, + }, + }, + { + label: "Baseline Peak (1.66 GB)", + data: [1.66], + backgroundColor: "rgba(251,191,36,0.7)", + borderRadius: 6, + datalabels: { + display: true, + anchor: "end" as const, + align: "right" as const, + formatter: (): string => "1.66 GB", + color: isDark ? DARK : DARK, + font: { size: 13, family: "Inter, system-ui, sans-serif" }, + }, + }, + { + label: "Current Peak (1.47 GB)", + data: [1.473], + backgroundColor: GREEN, + borderRadius: 6, + datalabels: { + display: true, + anchor: "end" as const, + align: "right" as const, + formatter: (): string => "1.47 GB peak", + color: isDark ? DARK : DARK, + font: { size: 14, family: "Inter, system-ui, sans-serif", weight: "bold" as const }, + }, + }, + ], + }), + [isDark], + ) + + return ( +
+ +
+ ) +} + +export function MaxAllocChart() { + const isDark = useIsDark() + const baseOptions = useChartOptions(isDark) + + const data = useMemo( + () => ({ + labels: ["Largest Single Allocation"], + datasets: [ + { + label: "Baseline", + data: [268], + backgroundColor: LIGHT_GRAY, + borderRadius: 6, + datalabels: { + display: true, + anchor: "end" as const, + align: "top" as const, + formatter: (v: number) => `${v} MB`, + color: isDark ? GRAY : "#71717a", + font: { size: 14, family: "Inter, system-ui, sans-serif" }, + }, + }, + { + label: "Current", + data: [134], + backgroundColor: GREEN, + borderRadius: 6, + datalabels: { + display: true, + anchor: "end" as const, + align: "top" as const, + formatter: (v: number) => `${v} MB`, + color: isDark ? GREEN : "#16a34a", + font: { size: 14, family: "Inter, system-ui, sans-serif" }, + }, + }, + ], + }), + [isDark], + ) + + return ( +
+ +
+ ) +} diff --git a/js/cf-webapp/src/components/membench/membench-toggle.tsx b/js/cf-webapp/src/components/membench/membench-toggle.tsx new file mode 100644 index 000000000..de25248a3 --- /dev/null +++ b/js/cf-webapp/src/components/membench/membench-toggle.tsx @@ -0,0 +1,46 @@ +"use client" + +import { useState } from "react" +import { cn } from "@/lib/utils" + +interface MembenchToggleProps { + execView: React.ReactNode + engView: React.ReactNode +} + +export function MembenchToggle({ execView, engView }: MembenchToggleProps) { + const [view, setView] = useState<"exec" | "eng">("exec") + + return ( + <> +
+
+ + +
+
+ + {view === "exec" ? execView : engView} + + ) +} diff --git a/js/cf-webapp/src/components/observability/parsed-response-view.tsx b/js/cf-webapp/src/components/observability/parsed-response-view.tsx index 2de97554f..6de47a10c 100644 --- a/js/cf-webapp/src/components/observability/parsed-response-view.tsx +++ b/js/cf-webapp/src/components/observability/parsed-response-view.tsx @@ -8,7 +8,7 @@ import { type ResponseSegment, } from "@/lib/observability-response-parse" import { CopyButton } from "@/components/observability/copy-button" -import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" +import { SyntaxHighlighter } from "@/lib/syntax-highlighter" import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism" import { useState } from "react" @@ -89,13 +89,7 @@ export function ParsedResponseView({ rawResponse, callType }: ParsedResponseView .map((id, i) => { const pos = i + 1 const label = - pos === 1 - ? "1st" - : pos === 2 - ? "2nd" - : pos === 3 - ? "3rd" - : `${pos}th` + pos === 1 ? "1st" : pos === 2 ? "2nd" : pos === 3 ? "3rd" : `${pos}th` return (
  • {label} - - {id.trim()} - + {id.trim()}
  • ) })} @@ -139,7 +131,10 @@ export function ParsedResponseView({ rawResponse, callType }: ParsedResponseView {segments.map((seg, i) => { const textJSON = seg.kind === "text" ? tryFormatJSON(seg.content) : null return seg.kind === "text" ? ( -
    +
    {textJSON ? ( ) : ( -
    +
    {seg.language || "code"} diff --git a/js/cf-webapp/src/components/report/report-charts.tsx b/js/cf-webapp/src/components/report/report-charts.tsx new file mode 100644 index 000000000..d98abe8a9 --- /dev/null +++ b/js/cf-webapp/src/components/report/report-charts.tsx @@ -0,0 +1,369 @@ +"use client" + +import { useMemo } from "react" +import { Bar, Doughnut } from "react-chartjs-2" +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + ArcElement, + Tooltip, + Legend, +} from "chart.js" +import ChartDataLabels from "chartjs-plugin-datalabels" + +ChartJS.register( + CategoryScale, + LinearScale, + BarElement, + ArcElement, + Tooltip, + Legend, + ChartDataLabels, +) + +/* ── colors matching the Dash app exactly ─────────────────────────── */ +const ACCENT = "#ffd227" // Codeflash brand yellow (Dash app primary) +const ACCENT_DIM = "#d08e0d" // amber-600 muted +const AMBER = "#fbbf24" // amber-400 +const AMBER_DARK = "#92400e" // amber-900 +const GREEN = "#4ade80" // green-400 +const BLUE = "#60a5fa" // blue-400 +const PURPLE = "#8B5CF6" // violet-500 +const GRAY = "#a1a1aa" // zinc-400 +const LIGHT_GRAY = "#71717a" // zinc-500 +const ZINC_700 = "#3f3f46" +const ZINC_400 = "#a1a1aa" +const SLATE = "#e4e4e7" // zinc-200 (Dash app primary text) + +function useIsDark() { + if (typeof window === "undefined") return true + return window.matchMedia("(prefers-color-scheme: dark)").matches +} + +function useChartOptions(isDark: boolean) { + return useMemo( + () => ({ + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: "top" as const, + labels: { + color: isDark ? ZINC_400 : "#52525b", + font: { family: "Inter, system-ui, sans-serif", size: 13 }, + }, + }, + datalabels: { + display: false as const, // off by default, each chart opts in + }, + }, + scales: { + x: { + ticks: { color: isDark ? ZINC_400 : "#71717a", font: { size: 12 } }, + grid: { display: false }, + border: { display: false }, + }, + y: { + ticks: { color: isDark ? ZINC_400 : "#71717a", font: { size: 12 } }, + grid: { color: isDark ? ZINC_700 : "#e4e4e7" }, + border: { display: false }, + }, + }, + }), + [isDark], + ) +} + +export function LatencyChart() { + const isDark = useIsDark() + const baseOptions = useChartOptions(isDark) + + const data = useMemo( + () => ({ + labels: ["Typical Response (p75)", "Worst Case (p95)"], + datasets: [ + { + label: "Before", + data: [289, 1242], + backgroundColor: LIGHT_GRAY, + borderRadius: 6, + datalabels: { + display: true, + anchor: "end" as const, + align: "top" as const, + formatter: (v: number) => `${v.toLocaleString()} ms`, + color: isDark ? GRAY : "#71717a", + font: { size: 13, family: "Inter, system-ui, sans-serif" }, + }, + }, + { + label: "After", + data: [216, 562], + backgroundColor: ACCENT, + borderRadius: 6, + datalabels: { + display: true, + anchor: "end" as const, + align: "top" as const, + formatter: (v: number) => `${v.toLocaleString()} ms`, + color: isDark ? ACCENT : "#b45309", + font: { size: 13, family: "Inter, system-ui, sans-serif" }, + }, + }, + ], + }), + [isDark], + ) + + return ( +
    + +
    + ) +} + +export function BundleChart() { + const isDark = useIsDark() + const baseOptions = useChartOptions(isDark) + + const data = useMemo( + () => ({ + labels: [ + "Error Monitoring\n(Sentry Replay)", + "Code Viewer\n(Syntax Highlighter)", + "Animations Library\n(framer-motion)", + "Syntax Theme\n(PrismLight)", + ], + datasets: [ + { + data: [600, 132, 70, 50], + backgroundColor: [ACCENT, ACCENT_DIM, AMBER, AMBER_DARK], + borderRadius: 6, + datalabels: { + display: true, + anchor: "end" as const, + align: "right" as const, + formatter: (v: number) => `${v} KB`, + color: isDark ? SLATE : "#27272a", + font: { size: 13, family: "Inter, system-ui, sans-serif" }, + }, + }, + ], + }), + [isDark], + ) + + return ( +
    + +
    + ) +} + +export function OnboardingChart() { + const isDark = useIsDark() + const baseOptions = useChartOptions(isDark) + + const data = useMemo( + () => ({ + labels: ["Average", "Worst Case (p95)"], + datasets: [ + { + label: "Before", + data: [10275, 20092], + backgroundColor: LIGHT_GRAY, + borderRadius: 6, + datalabels: { + display: true, + anchor: "end" as const, + align: "top" as const, + formatter: (v: number) => `${(v / 1000).toFixed(1)}s`, + color: isDark ? GRAY : "#71717a", + font: { size: 13, family: "Inter, system-ui, sans-serif" }, + }, + }, + { + label: "After", + data: [8108, 8108], + backgroundColor: GREEN, + borderRadius: 6, + datalabels: { + display: true, + anchor: "end" as const, + align: "top" as const, + formatter: (v: number) => `${(v / 1000).toFixed(1)}s`, + color: isDark ? GREEN : "#16a34a", + font: { size: 13, family: "Inter, system-ui, sans-serif" }, + }, + }, + ], + }), + [isDark], + ) + + return ( +
    + +
    + ) +} + +export function CategoryPie() { + const isDark = useIsDark() + + const data = useMemo( + () => ({ + labels: [ + "Faster Load Times", + "Smaller Downloads", + "Better Monitoring", + "Testing & CI", + "Cleanup", + ], + datasets: [ + { + data: [7, 6, 4, 2, 1], + backgroundColor: [GREEN, BLUE, AMBER, PURPLE, GRAY], + borderWidth: 0, + }, + ], + }), + [], + ) + + // Custom plugin for center text + const centerTextPlugin = useMemo( + () => ({ + id: "centerText", + afterDraw(chart: ChartJS) { + const { ctx, chartArea } = chart + const centerX = (chartArea.left + chartArea.right) / 2 + const centerY = (chartArea.top + chartArea.bottom) / 2 + + ctx.save() + ctx.textAlign = "center" + ctx.textBaseline = "middle" + + // "20" number + ctx.font = "bold 20px Inter, system-ui, sans-serif" + ctx.fillStyle = isDark ? SLATE : "#27272a" + ctx.fillText("20", centerX, centerY - 8) + + // "changes" label + ctx.font = "13px Inter, system-ui, sans-serif" + ctx.fillStyle = isDark ? GRAY : "#71717a" + ctx.fillText("changes", centerX, centerY + 12) + + ctx.restore() + }, + }), + [isDark], + ) + + return ( +
    + `${ctx.label}: ${ctx.parsed} changes`, + }, + }, + datalabels: { + display: true, + color: isDark ? SLATE : "#27272a", + font: { size: 12, family: "Inter, system-ui, sans-serif" }, + formatter: (_value: number, ctx) => { + const label = ctx.chart.data.labels?.[ctx.dataIndex] ?? "" + const value = ctx.chart.data.datasets[0].data[ctx.dataIndex] + return `${label}\n${value}` + }, + textAlign: "center" as const, + anchor: "end" as const, + align: "end" as const, + offset: 4, + }, + }, + }} + /> +
    + ) +} diff --git a/js/cf-webapp/src/components/report/report-toggle.tsx b/js/cf-webapp/src/components/report/report-toggle.tsx new file mode 100644 index 000000000..345f94824 --- /dev/null +++ b/js/cf-webapp/src/components/report/report-toggle.tsx @@ -0,0 +1,46 @@ +"use client" + +import { useState } from "react" +import { cn } from "@/lib/utils" + +interface ReportToggleProps { + execView: React.ReactNode + engView: React.ReactNode +} + +export function ReportToggle({ execView, engView }: ReportToggleProps) { + const [view, setView] = useState<"exec" | "eng">("exec") + + return ( + <> +
    +
    + + +
    +
    + + {view === "exec" ? execView : engView} + + ) +} diff --git a/js/cf-webapp/src/components/trace/monaco-diff-viewer.tsx b/js/cf-webapp/src/components/trace/monaco-diff-viewer.tsx index fc1771cac..389a37a53 100644 --- a/js/cf-webapp/src/components/trace/monaco-diff-viewer.tsx +++ b/js/cf-webapp/src/components/trace/monaco-diff-viewer.tsx @@ -3,30 +3,31 @@ import React, { useState, useEffect, useMemo, useCallback, useRef } from "react" import { DiffEditor, useMonaco, DiffOnMount } from "@monaco-editor/react" import { + AlertTriangle, + BarChart3, CheckCircle2, - XCircle, - GitPullRequest, - Zap, - TestTube, ChevronDown, ChevronUp, + Edit3, ExternalLink, FileCode, - Edit3, - Save, - X, + FileText, + GitPullRequest, + Loader2, Lock, Monitor, + Save, Smartphone, - BarChart3, + TestTube, + X, + XCircle, + Zap, } from "lucide-react" -// Ensure you have lucide-react installed as per your package.json -import { Loader2, FileText, AlertTriangle } from "lucide-react" import type { ExperimentMetadata, DiffContents } from "@/lib/types" // Adjust path if needed import { getMonacoLanguage } from "@/lib/utils" import ReactMarkdown from "react-markdown" import remarkGfm from "remark-gfm" -import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" +import { SyntaxHighlighter } from "@/lib/syntax-highlighter" import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism" interface MonacoDiffViewerProps { diff --git a/js/cf-webapp/src/instrumentation-client.ts b/js/cf-webapp/src/instrumentation-client.ts index 89483ef17..386064fbe 100644 --- a/js/cf-webapp/src/instrumentation-client.ts +++ b/js/cf-webapp/src/instrumentation-client.ts @@ -14,8 +14,8 @@ Sentry.init({ ? "https://0fa0f40b2d709e4f1eb9aac76ff9e6be@o4506833230561280.ingest.us.sentry.io/4506833279582208" : undefined, - // Adjust this value in production, or use tracesSampler for greater control - tracesSampleRate: 1, + // Sample 10% in production, 100% in dev + tracesSampleRate: isProduction ? 0.1 : 1, // Setting this option to true will print useful information to the console while you're setting up Sentry. debug: false, @@ -26,12 +26,18 @@ Sentry.init({ // in development and sample at a lower rate in production replaysSessionSampleRate: 0.1, - // You can remove this option if you're not planning to use the Sentry Session Replay feature: - integrations: [ - Sentry.replayIntegration({ - // Additional Replay configuration goes in here, for example: + // Replay is NOT included here — it's lazy-loaded below to keep it out of + // the initial bundle (~300 KB per copy, two copies were previously shipped). + integrations: [Sentry.browserTracingIntegration({ enableLongAnimationFrame: true })], +}) + +// Lazy-load Session Replay so the ~300 KB replay bundle is fetched after +// the page is interactive rather than blocking initial load. +Sentry.lazyLoadIntegration("replayIntegration").then((replayIntegration) => { + Sentry.addIntegration( + replayIntegration({ maskAllText: true, blockAllMedia: true, }), - ], + ) }) diff --git a/js/cf-webapp/src/instrumentation.ts b/js/cf-webapp/src/instrumentation.ts index 86830ab5c..095be8d73 100644 --- a/js/cf-webapp/src/instrumentation.ts +++ b/js/cf-webapp/src/instrumentation.ts @@ -1,10 +1,43 @@ import * as Sentry from "@sentry/nextjs" -export function register() { - // Sentry initialization is now handled by dedicated config files: - // - sentry.server.config.ts for server-side - // - sentry.client.config.ts for client-side - // This prevents duplicate initialization issues with Sentry v9 +const otelEnabled = + process.env.NODE_ENV === "production" || + process.env.OTEL_ENABLED === "true" + +export async function register() { + if (!otelEnabled) return + if (process.env.NEXT_RUNTIME !== "nodejs") return + + // Dynamic imports so OTel packages are only loaded when tracing is active + const { NodeSDK } = await import("@opentelemetry/sdk-node") + const { getNodeAutoInstrumentations } = await import( + "@opentelemetry/auto-instrumentations-node" + ) + const { PrismaInstrumentation } = await import("@prisma/instrumentation") + const { + SentrySpanProcessor, + SentryPropagator, + SentrySampler, + } = await import("@sentry/opentelemetry") + + const sentryClient = Sentry.getClient() + + const sdk = new NodeSDK({ + sampler: sentryClient ? new SentrySampler(sentryClient) : undefined, + spanProcessors: [new SentrySpanProcessor()], + textMapPropagator: new SentryPropagator(), + instrumentations: [ + getNodeAutoInstrumentations({ + // Disable noisy/low-value instrumentations + "@opentelemetry/instrumentation-fs": { enabled: false }, + "@opentelemetry/instrumentation-dns": { enabled: false }, + "@opentelemetry/instrumentation-net": { enabled: false }, + }), + new PrismaInstrumentation(), + ], + }) + + sdk.start() } export const onRequestError = Sentry.captureRequestError diff --git a/js/cf-webapp/src/lib/__tests__/server-action-timing.test.ts b/js/cf-webapp/src/lib/__tests__/server-action-timing.test.ts new file mode 100644 index 000000000..ff87c736a --- /dev/null +++ b/js/cf-webapp/src/lib/__tests__/server-action-timing.test.ts @@ -0,0 +1,148 @@ +import { describe, it, expect, vi, beforeEach } from "vitest" +import * as Sentry from "@sentry/nextjs" +import { withTiming } from "../server-action-timing" + +describe("withTiming", () => { + let mockSetAttribute: ReturnType + + beforeEach(() => { + mockSetAttribute = vi.fn() + vi.mocked(Sentry.startSpan).mockImplementation((_opts, callback) => + callback({ setAttribute: mockSetAttribute } as any), + ) + }) + + describe("successful execution", () => { + it("returns the wrapped function's result", async () => { + const inner = vi.fn().mockResolvedValue({ data: "hello" }) + const wrapped = withTiming("test-action", inner) + + const result = await wrapped("arg1") + expect(result).toEqual({ data: "hello" }) + }) + + it("passes arguments through to the wrapped function", async () => { + const inner = vi.fn().mockResolvedValue(null) + const wrapped = withTiming("test-action", inner) + + await wrapped("a", 42, true) + expect(inner).toHaveBeenCalledWith("a", 42, true) + }) + + it("creates a Sentry span with correct name and op", async () => { + const wrapped = withTiming("myAction", vi.fn().mockResolvedValue(null)) + await wrapped() + + expect(Sentry.startSpan).toHaveBeenCalledWith( + expect.objectContaining({ + name: "myAction", + op: "server.action", + attributes: { "server_action.name": "myAction" }, + }), + expect.any(Function), + ) + }) + + it("sets duration_ms attribute on span", async () => { + const nowSpy = vi.spyOn(performance, "now") + nowSpy.mockReturnValueOnce(0).mockReturnValueOnce(250) + + const wrapped = withTiming("test", vi.fn().mockResolvedValue(null)) + await wrapped() + + expect(mockSetAttribute).toHaveBeenCalledWith( + "server_action.duration_ms", + 250, + ) + nowSpy.mockRestore() + }) + }) + + describe("slow action detection", () => { + it("logs console.warn when duration exceeds 1000ms", async () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) + const nowSpy = vi.spyOn(performance, "now") + nowSpy.mockReturnValueOnce(0).mockReturnValueOnce(1500) + + const wrapped = withTiming("slowAction", vi.fn().mockResolvedValue(null)) + await wrapped() + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("slowAction"), + ) + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("1500"), + ) + + warnSpy.mockRestore() + nowSpy.mockRestore() + }) + + it("sets server_action.slow attribute when slow", async () => { + vi.spyOn(console, "warn").mockImplementation(() => {}) + const nowSpy = vi.spyOn(performance, "now") + nowSpy.mockReturnValueOnce(0).mockReturnValueOnce(2000) + + const wrapped = withTiming("slow", vi.fn().mockResolvedValue(null)) + await wrapped() + + expect(mockSetAttribute).toHaveBeenCalledWith("server_action.slow", true) + + nowSpy.mockRestore() + }) + + it("does not warn for fast actions", async () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) + const nowSpy = vi.spyOn(performance, "now") + nowSpy.mockReturnValueOnce(0).mockReturnValueOnce(500) + + const wrapped = withTiming("fast", vi.fn().mockResolvedValue(null)) + await wrapped() + + expect(warnSpy).not.toHaveBeenCalled() + + warnSpy.mockRestore() + nowSpy.mockRestore() + }) + }) + + describe("error handling", () => { + it("re-throws the original error", async () => { + const error = new Error("boom") + vi.spyOn(console, "error").mockImplementation(() => {}) + const wrapped = withTiming("fail", vi.fn().mockRejectedValue(error)) + + await expect(wrapped()).rejects.toThrow("boom") + }) + + it("calls Sentry.captureException with error and tags", async () => { + const error = new Error("db failed") + vi.spyOn(console, "error").mockImplementation(() => {}) + const wrapped = withTiming("dbAction", vi.fn().mockRejectedValue(error)) + + await expect(wrapped()).rejects.toThrow() + + expect(Sentry.captureException).toHaveBeenCalledWith(error, { + tags: { server_action: "dbAction" }, + extra: { duration_ms: expect.any(Number) }, + }) + }) + + it("logs error with duration via console.error", async () => { + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + const nowSpy = vi.spyOn(performance, "now") + nowSpy.mockReturnValueOnce(0).mockReturnValueOnce(300) + + const wrapped = withTiming("errAction", vi.fn().mockRejectedValue(new Error("oops"))) + await expect(wrapped()).rejects.toThrow() + + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining("errAction"), + expect.any(Error), + ) + + errorSpy.mockRestore() + nowSpy.mockRestore() + }) + }) +}) diff --git a/js/cf-webapp/src/lib/analytics/tracking.ts b/js/cf-webapp/src/lib/analytics/tracking.ts index f98756e32..27db9a8d3 100644 --- a/js/cf-webapp/src/lib/analytics/tracking.ts +++ b/js/cf-webapp/src/lib/analytics/tracking.ts @@ -37,7 +37,7 @@ export async function trackUserLogin(userData: { }) // Ensure events are sent - await posthog?.shutdown() + await posthog?.flush() console.log(`[Analytics] Tracked login for user ${userData.userId}`) } catch (error) { @@ -45,3 +45,87 @@ export async function trackUserLogin(userData: { console.error("[Analytics] Failed to track login:", error) } } + +// --- New tracking events for key user journeys --- + +/** + * Captures a single event and flushes. All tracking helpers funnel through this + * so we have one place to handle errors / shutdown. + */ +async function captureEvent( + distinctId: string, + event: string, + properties?: Record, +) { + try { + const posthog = getPostHogClient() + if (!posthog) return + + posthog.capture({ + distinctId, + event, + properties: { + ...properties, + timestamp: new Date().toISOString(), + }, + }) + + await posthog.shutdown() + } catch (error) { + // Never let tracking errors break the calling flow + console.error(`[Analytics] Failed to track ${event}:`, error) + } +} + +export async function trackOptimizationReviewed( + userId: string, + properties: { + traceId: string + functionName?: string | null + repositoryName?: string | null + status?: string | null + }, +) { + await captureEvent(userId, "optimization_reviewed", properties) +} + +export async function trackRepositoryConnected( + userId: string, + properties: { + repositoryId: string + repositoryName: string + }, +) { + await captureEvent(userId, "repository_connected", properties) +} + +export async function trackApiKeyCreated( + userId: string, + properties: { + keyName: string + organizationId?: string + }, +) { + await captureEvent(userId, "api_key_created", properties) +} + +export async function trackMemberInvited( + userId: string, + properties: { + invitedUsername: string + role: string + scope: "organization" | "repository" + targetId: string + }, +) { + await captureEvent(userId, "member_invited", properties) +} + +export async function trackBillingPageViewed( + userId: string, + properties?: { + username?: string + }, +) { + await captureEvent(userId, "billing_page_viewed", properties) +} diff --git a/js/cf-webapp/src/lib/modified-code-utils.ts b/js/cf-webapp/src/lib/modified-code-utils.ts index f46eceac1..262d2cbbc 100644 --- a/js/cf-webapp/src/lib/modified-code-utils.ts +++ b/js/cf-webapp/src/lib/modified-code-utils.ts @@ -1,7 +1,5 @@ -import { PrismaClient } from "@prisma/client" import { ExperimentMetadata } from "./types" - -const prisma = new PrismaClient() +import { prisma } from "@/lib/prisma" /** * Get the modified code for a trace, falling back to original optimized code if no modifications exist diff --git a/js/cf-webapp/src/lib/posthog.ts b/js/cf-webapp/src/lib/posthog.ts index 566d80c70..44864b442 100644 --- a/js/cf-webapp/src/lib/posthog.ts +++ b/js/cf-webapp/src/lib/posthog.ts @@ -1,12 +1,17 @@ import { PostHog } from "posthog-node" +let client: PostHog | undefined + export default function PostHogClient(): PostHog | undefined { if (process.env.NODE_ENV !== "production") { return undefined } - return new PostHog("phc_aUO790jHd7z1SXwsYCz8dRApxueplZlZWeDSpKc5hol", { - host: "https://app.posthog.com", - flushAt: 1, - flushInterval: 0, - }) + if (!client) { + client = new PostHog("phc_aUO790jHd7z1SXwsYCz8dRApxueplZlZWeDSpKc5hol", { + host: "https://app.posthog.com", + flushAt: 1, + flushInterval: 0, + }) + } + return client } diff --git a/js/cf-webapp/src/lib/prisma.ts b/js/cf-webapp/src/lib/prisma.ts index ff5f0314d..69aae284a 100644 --- a/js/cf-webapp/src/lib/prisma.ts +++ b/js/cf-webapp/src/lib/prisma.ts @@ -1,9 +1,13 @@ import { PrismaClient } from "@prisma/client" +import * as Sentry from "@sentry/nextjs" const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined } +const isProduction = process.env.NODE_ENV === "production" +const SLOW_QUERY_THRESHOLD_MS = 500 + function buildDatabaseUrl() { const baseUrl = process.env.DATABASE_URL ?? "" if (baseUrl.includes("connection_limit")) return baseUrl @@ -14,9 +18,37 @@ function buildDatabaseUrl() { export const prisma = globalForPrisma.prisma ?? new PrismaClient({ + log: isProduction + ? [ + { emit: "event", level: "warn" }, + { emit: "event", level: "error" }, + ] + : [ + { emit: "event", level: "query" }, + { emit: "event", level: "warn" }, + { emit: "event", level: "error" }, + ], datasources: { db: { url: buildDatabaseUrl() }, }, }) -if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma +// Log slow queries in development +if (!isProduction) { + ;(prisma as any).$on("query", (e: any) => { + if (e.duration > SLOW_QUERY_THRESHOLD_MS) { + console.warn(`[Prisma] Slow query (${e.duration}ms): ${e.query}`) + } + }) +} + +// Forward Prisma warnings and errors to Sentry +;(prisma as any).$on("warn", (e: any) => { + console.warn("[Prisma] Warning:", e.message) +}) +;(prisma as any).$on("error", (e: any) => { + console.error("[Prisma] Error:", e.message) + Sentry.captureException(new Error(`Prisma error: ${e.message}`)) +}) + +if (!isProduction) globalForPrisma.prisma = prisma diff --git a/js/cf-webapp/src/lib/server-action-timing.ts b/js/cf-webapp/src/lib/server-action-timing.ts new file mode 100644 index 000000000..0b00e2228 --- /dev/null +++ b/js/cf-webapp/src/lib/server-action-timing.ts @@ -0,0 +1,57 @@ +import * as Sentry from "@sentry/nextjs" + +const SLOW_ACTION_THRESHOLD_MS = 1000 + +/** + * Wraps a server action with performance timing instrumentation. + * Measures execution duration, logs slow actions (>1s) as warnings, + * and reports timing to Sentry as custom spans. + */ +export function withTiming( + actionName: string, + fn: (...args: TArgs) => Promise, +): (...args: TArgs) => Promise { + return async (...args: TArgs): Promise => { + const start = performance.now() + + try { + const result = await Sentry.startSpan( + { + name: actionName, + op: "server.action", + attributes: { + "server_action.name": actionName, + }, + }, + async (span) => { + const res = await fn(...args) + const durationMs = performance.now() - start + + span.setAttribute("server_action.duration_ms", durationMs) + + if (durationMs > SLOW_ACTION_THRESHOLD_MS) { + console.warn( + `[ServerAction] Slow action: ${actionName} took ${durationMs.toFixed(0)}ms`, + ) + span.setAttribute("server_action.slow", true) + } + + return res + }, + ) + + return result + } catch (error) { + const durationMs = performance.now() - start + console.error( + `[ServerAction] ${actionName} failed after ${durationMs.toFixed(0)}ms:`, + error, + ) + Sentry.captureException(error, { + tags: { server_action: actionName }, + extra: { duration_ms: durationMs }, + }) + throw error + } + } +} diff --git a/js/cf-webapp/src/lib/services/github-service.ts b/js/cf-webapp/src/lib/services/github-service.ts index 77e532734..0b6fc435c 100644 --- a/js/cf-webapp/src/lib/services/github-service.ts +++ b/js/cf-webapp/src/lib/services/github-service.ts @@ -1,4 +1,4 @@ -import * as Sentry from "@sentry/browser" +import * as Sentry from "@sentry/nextjs" import { ActionResponse, createErrorResponse, createSuccessResponse } from "../action-response" import { GitHubUserSearchResult } from "../types" diff --git a/js/cf-webapp/src/lib/syntax-highlighter.ts b/js/cf-webapp/src/lib/syntax-highlighter.ts new file mode 100644 index 000000000..81c078764 --- /dev/null +++ b/js/cf-webapp/src/lib/syntax-highlighter.ts @@ -0,0 +1,36 @@ +/** + * Lightweight syntax highlighter using PrismLight with only the languages + * this app needs, instead of the full Prism build that ships all 300 grammars. + * + * Usage: + * import { SyntaxHighlighter } from "@/lib/syntax-highlighter" + * + * Saves ~850 KB of client JS by not shipping unused language grammars. + */ +import SyntaxHighlighter from "react-syntax-highlighter/dist/esm/prism-light" +import python from "react-syntax-highlighter/dist/esm/languages/prism/python" +import javascript from "react-syntax-highlighter/dist/esm/languages/prism/javascript" +import typescript from "react-syntax-highlighter/dist/esm/languages/prism/typescript" +import java from "react-syntax-highlighter/dist/esm/languages/prism/java" +import json from "react-syntax-highlighter/dist/esm/languages/prism/json" +import css from "react-syntax-highlighter/dist/esm/languages/prism/css" +import markup from "react-syntax-highlighter/dist/esm/languages/prism/markup" +import bash from "react-syntax-highlighter/dist/esm/languages/prism/bash" +import jsx from "react-syntax-highlighter/dist/esm/languages/prism/jsx" +import tsx from "react-syntax-highlighter/dist/esm/languages/prism/tsx" + +SyntaxHighlighter.registerLanguage("python", python) +SyntaxHighlighter.registerLanguage("javascript", javascript) +SyntaxHighlighter.registerLanguage("typescript", typescript) +SyntaxHighlighter.registerLanguage("java", java) +SyntaxHighlighter.registerLanguage("json", json) +SyntaxHighlighter.registerLanguage("css", css) +SyntaxHighlighter.registerLanguage("html", markup) +SyntaxHighlighter.registerLanguage("markup", markup) +SyntaxHighlighter.registerLanguage("bash", bash) +SyntaxHighlighter.registerLanguage("jsx", jsx) +SyntaxHighlighter.registerLanguage("tsx", tsx) +SyntaxHighlighter.registerLanguage("plaintext", () => ({})) +SyntaxHighlighter.registerLanguage("text", () => ({})) + +export { SyntaxHighlighter } diff --git a/js/cf-webapp/src/lib/team-members.ts b/js/cf-webapp/src/lib/team-members.ts index 54529d208..dadfa4786 100644 --- a/js/cf-webapp/src/lib/team-members.ts +++ b/js/cf-webapp/src/lib/team-members.ts @@ -29,3 +29,10 @@ export function isTeamMemberCheck(user: { email?: string; nickname?: string }): (nickname !== undefined && TEAM_MEMBERS.has(nickname)) ) } + +/** Codeflash team members OR anyone with an @unstructured.io email */ +export function isMembenchAllowed(user: { email?: string; nickname?: string }): boolean { + if (isTeamMemberCheck(user)) return true + const email = user.email?.toLowerCase() + return email !== undefined && email.endsWith("@unstructured.io") +} diff --git a/js/cf-webapp/src/proxy.ts b/js/cf-webapp/src/proxy.ts index 3a8ebc4b1..a63611718 100644 --- a/js/cf-webapp/src/proxy.ts +++ b/js/cf-webapp/src/proxy.ts @@ -1,6 +1,6 @@ import { type NextRequest, NextResponse } from "next/server" import { auth0 } from "@/lib/auth0" -import { isTeamMemberCheck } from "@/lib/team-members" +import { isTeamMemberCheck, isMembenchAllowed } from "@/lib/team-members" export async function proxy(req: NextRequest) { // Let Auth0 handle auth routes (/auth/login, /auth/callback, /auth/logout, etc.) @@ -46,12 +46,22 @@ export async function proxy(req: NextRequest) { return NextResponse.redirect(loginUrl) } - if (pathname.startsWith("/observability")) { + if ( + pathname.startsWith("/observability") || + pathname.startsWith("/roadmap") || + pathname.startsWith("/report") + ) { if (!isTeamMemberCheck(session.user)) { return NextResponse.redirect(new URL("/", origin)) } } + if (pathname.startsWith("/membench")) { + if (!isMembenchAllowed(session.user)) { + return NextResponse.redirect(new URL("/", origin)) + } + } + return authRes } diff --git a/js/cf-webapp/src/test/setup.ts b/js/cf-webapp/src/test/setup.ts new file mode 100644 index 000000000..62633a9b7 --- /dev/null +++ b/js/cf-webapp/src/test/setup.ts @@ -0,0 +1,75 @@ +import { vi, afterEach } from "vitest" + +// --------------------------------------------------------------------------- +// Mock: @codeflash-ai/common (Prisma + utility functions) +// --------------------------------------------------------------------------- +vi.mock("@codeflash-ai/common", () => { + const mockPrisma = { + organizations: { + findFirst: vi.fn(), + }, + optimization_events: { + findMany: vi.fn(), + findFirst: vi.fn(), + count: vi.fn(), + groupBy: vi.fn(), + }, + optimization_features: { + findMany: vi.fn(), + findUnique: vi.fn(), + }, + repositories: { + findFirst: vi.fn(), + }, + organization_members: { + findFirst: vi.fn(), + create: vi.fn(), + update: vi.fn(), + }, + repository_members: { + create: vi.fn(), + delete: vi.fn(), + }, + users: { + create: vi.fn(), + }, + $queryRawUnsafe: vi.fn(), + $disconnect: vi.fn(), + } + + return { + prisma: mockPrisma, + buildOptimizationOrCondition: vi.fn().mockReturnValue({}), + getUserById: vi.fn(), + createOrUpdateUser: vi.fn(), + deleteOrganizationMemberApiKeys: vi.fn(), + organizationMemberRepository: { + delete: vi.fn(), + }, + } +}) + +// --------------------------------------------------------------------------- +// Mock: @sentry/nextjs +// --------------------------------------------------------------------------- +vi.mock("@sentry/nextjs", () => ({ + startSpan: vi.fn((_opts: any, callback: any) => + callback({ setAttribute: vi.fn() }), + ), + captureException: vi.fn(), +})) + +// --------------------------------------------------------------------------- +// Mock: @sentry/node +// --------------------------------------------------------------------------- +vi.mock("@sentry/node", () => ({ + captureException: vi.fn(), + captureMessage: vi.fn(), +})) + +// --------------------------------------------------------------------------- +// Clear all mocks between tests +// --------------------------------------------------------------------------- +afterEach(() => { + vi.clearAllMocks() +}) diff --git a/js/cf-webapp/vitest.config.ts b/js/cf-webapp/vitest.config.ts index be8ea78d2..87941cc57 100644 --- a/js/cf-webapp/vitest.config.ts +++ b/js/cf-webapp/vitest.config.ts @@ -1,9 +1,16 @@ import { defineConfig } from "vitest/config" import react from "@vitejs/plugin-react" +import path from "path" export default defineConfig({ plugins: [react()] as any, + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, test: { environment: "jsdom", + setupFiles: ["./src/test/setup.ts"], }, })