xtablo-source/docs/superpowers/plans/2026-04-22-datadog-rum-sourcemaps-via-ci.md
2026-04-22 22:13:30 +02:00

12 KiB
Raw Blame History

Datadog RUM Sourcemaps Via CI Implementation Plan

For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Generate private frontend sourcemaps for Datadog RUM/Error Tracking, upload them from GitHub Actions, and keep .map files out of deployed frontend assets.

Architecture: Add an explicit release-version contract to the main apps RUM init, enable hidden sourcemaps in all Vite frontends, and create a frontend CI workflow that builds each app, uploads sourcemaps with matching Datadog metadata, and removes .map files before any deploy step consumes the build output.

Tech Stack: React 19, Vite 6, Vitest, GitHub Actions, Datadog CI CLI, Cloudflare/Wrangler deploy targets, pnpm workspaces.

Spec: docs/superpowers/specs/2026-04-22-datadog-rum-sourcemaps-via-ci-design.md


File Structure

New files

  • .github/workflows/frontend-sourcemaps.yml — GitHub Actions workflow that builds the frontend apps on a self-hosted runner and uploads sourcemaps to Datadog.
  • apps/clients/src/vite-env.d.ts — Vite env typings for client-specific build variables, including VITE_APP_VERSION.
  • apps/main/src/lib/rum.test.ts — regression test for Datadog RUM init config.
  • apps/main/vite.config.test.ts — production sourcemap config test for apps/main.
  • apps/clients/vite.config.test.ts — production sourcemap config test for apps/clients.
  • apps/external/vite.config.test.ts — production sourcemap config test for apps/external.

Modified files

  • package.json — add @datadog/datadog-ci to root dev dependencies.
  • apps/main/src/lib/rum.ts — add version: import.meta.env.VITE_APP_VERSION.
  • apps/main/src/vite-env.d.ts — add VITE_APP_VERSION typing.
  • apps/external/src/vite-env.d.ts — add VITE_APP_VERSION typing.
  • apps/main/vite.config.ts — enable hidden sourcemaps for non-test builds.
  • apps/clients/vite.config.ts — enable hidden sourcemaps for non-test builds.
  • apps/external/vite.config.ts — enable hidden sourcemaps for non-test builds.

Chunk 1: Runtime Release Metadata

Task 1: Wire Datadog RUM version in apps/main

Files:

  • Create: apps/main/src/lib/rum.test.ts

  • Modify: apps/main/src/lib/rum.ts

  • Modify: apps/main/src/vite-env.d.ts

  • Step 1: Write the failing RUM init test

Create apps/main/src/lib/rum.test.ts:

import { beforeEach, describe, expect, it, vi } from "vitest";

const init = vi.fn();

vi.mock("@datadog/browser-rum", () => ({
  datadogRum: {
    init,
  },
}));

vi.mock("@datadog/browser-rum-react", () => ({
  reactPlugin: () => "react-plugin",
}));

describe("rum config", () => {
  beforeEach(() => {
    init.mockReset();
    vi.resetModules();
  });

  it("sets the Datadog release version from VITE_APP_VERSION", async () => {
    vi.stubEnv("VITE_APP_VERSION", "test-sha");
    vi.stubEnv("MODE", "production");

    await import("./rum");

    expect(init).toHaveBeenCalledWith(
      expect.objectContaining({
        service: "xtablo-ui",
        env: "production",
        version: "test-sha",
      })
    );
  });
});
  • Step 2: Run the test to verify it fails

Run:

pnpm --filter @xtablo/main exec vitest run src/lib/rum.test.ts --mode dev

Expected: FAIL because version is not set in datadogRum.init.

  • Step 3: Add the runtime version

Update apps/main/src/lib/rum.ts:

  service: "xtablo-ui",
  env: import.meta.env.MODE,
  version: import.meta.env.VITE_APP_VERSION,

Update apps/main/src/vite-env.d.ts:

/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_APP_VERSION: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}
  • Step 4: Run the test to verify it passes

Run:

pnpm --filter @xtablo/main exec vitest run src/lib/rum.test.ts --mode dev
pnpm --filter @xtablo/main typecheck

Expected: PASS.

  • Step 5: Commit
git add apps/main/src/lib/rum.ts apps/main/src/lib/rum.test.ts apps/main/src/vite-env.d.ts
git commit -m "feat: add Datadog release version to main rum config"

Chunk 2: Hidden Sourcemaps In All Vite Frontends

Task 2: Add production sourcemap config tests

Files:

  • Create: apps/main/vite.config.test.ts

  • Create: apps/clients/vite.config.test.ts

  • Create: apps/external/vite.config.test.ts

  • Step 1: Write the failing config test for apps/main

Create apps/main/vite.config.test.ts:

import { describe, expect, it } from "vitest";
import configFactory from "./vite.config";

describe("main vite config", () => {
  it("uses hidden sourcemaps for production builds", () => {
    const config = configFactory({ mode: "production" });
    expect(config.build?.sourcemap).toBe("hidden");
  });
});
  • Step 2: Write the failing config test for apps/clients

Create apps/clients/vite.config.test.ts:

import { describe, expect, it } from "vitest";
import configFactory from "./vite.config";

describe("clients vite config", () => {
  it("uses hidden sourcemaps for production builds", () => {
    const config = configFactory({ mode: "production" });
    expect(config.build?.sourcemap).toBe("hidden");
  });
});
  • Step 3: Write the failing config test for apps/external

Create apps/external/vite.config.test.ts:

import { describe, expect, it } from "vitest";
import configFactory from "./vite.config";

describe("external vite config", () => {
  it("uses hidden sourcemaps for production builds", () => {
    const config = configFactory({ mode: "production" });
    expect(config.build?.sourcemap).toBe("hidden");
  });
});
  • Step 4: Run the tests to verify they fail

Run:

pnpm --filter @xtablo/main exec vitest run vite.config.test.ts --mode dev
pnpm --filter @xtablo/clients exec vitest run vite.config.test.ts --mode test
pnpm --filter @xtablo/external exec vitest run vite.config.test.ts --mode test

Expected: FAIL because build.sourcemap is not configured.

  • Step 5: Implement hidden sourcemaps in all three Vite configs

Update:

  • apps/main/vite.config.ts
  • apps/clients/vite.config.ts
  • apps/external/vite.config.ts

Add the same build section in each returned config:

    build: {
      sourcemap: mode === "test" ? false : "hidden",
    },

Keep it alongside the existing plugins, server, define, and test sections.

  • Step 6: Add env typing for the remaining apps

Create apps/clients/src/vite-env.d.ts:

/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_APP_VERSION: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}

Update apps/external/src/vite-env.d.ts:

/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_APP_VERSION: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}
  • Step 7: Run the tests and typechecks

Run:

pnpm --filter @xtablo/main exec vitest run vite.config.test.ts --mode dev
pnpm --filter @xtablo/clients exec vitest run vite.config.test.ts --mode test
pnpm --filter @xtablo/external exec vitest run vite.config.test.ts --mode test
pnpm --filter @xtablo/main typecheck
pnpm --filter @xtablo/clients typecheck
pnpm --filter @xtablo/external typecheck

Expected: PASS.

  • Step 8: Commit
git add apps/main/vite.config.ts apps/main/vite.config.test.ts apps/clients/vite.config.ts apps/clients/vite.config.test.ts apps/clients/src/vite-env.d.ts apps/external/vite.config.ts apps/external/vite.config.test.ts apps/external/src/vite-env.d.ts
git commit -m "build: enable hidden sourcemaps for frontend apps"

Chunk 3: Datadog CI Upload Workflow

Task 3: Add pinned Datadog CLI and frontend sourcemap workflow

Files:

  • Create: .github/workflows/frontend-sourcemaps.yml

  • Modify: package.json

  • Step 1: Add the Datadog CLI dependency

Update root package.json:

  "devDependencies": {
    "@biomejs/biome": "2.2.5",
    "@datadog/datadog-ci": "^3.21.0",
    "turbo": "^2.5.8",
    "typescript": "^5.7.0"
  }
  • Step 2: Install dependencies and verify lockfile changes

Run:

pnpm install

Expected: pnpm-lock.yaml updates with @datadog/datadog-ci.

  • Step 3: Create the workflow

Create .github/workflows/frontend-sourcemaps.yml:

name: Frontend Sourcemaps

on:
  workflow_dispatch:
  push:
    branches:
      - main
      - develop

jobs:
  upload-sourcemaps:
    runs-on:
      - self-hosted
      - linux
      - x64
    env:
      DATADOG_API_KEY: ${{ secrets.DATADOG_API_KEY }}
      DATADOG_SITE: ${{ secrets.DATADOG_SITE }}
      RELEASE_VERSION: ${{ github.sha }}
    steps:
      - uses: actions/checkout@v6

      - uses: pnpm/action-setup@v5
        with:
          version: 10.19.0

      - uses: actions/setup-node@v6
        with:
          node-version: 22
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile --child-concurrency=2

      - name: Build main
        run: pnpm --filter @xtablo/main build:prod
        env:
          VITE_APP_VERSION: ${{ env.RELEASE_VERSION }}

      - name: Upload main sourcemaps
        run: pnpm exec datadog-ci sourcemaps upload apps/main/dist --service xtablo-ui --release-version "$RELEASE_VERSION" --minified-path-prefix https://app.xtablo.com/assets

      - name: Remove main sourcemaps
        run: find apps/main/dist -name '*.map' -delete

      - name: Build clients
        run: pnpm --filter @xtablo/clients build:prod
        env:
          VITE_APP_VERSION: ${{ env.RELEASE_VERSION }}

      - name: Upload clients sourcemaps
        run: pnpm exec datadog-ci sourcemaps upload apps/clients/dist --service xtablo-clients --release-version "$RELEASE_VERSION" --minified-path-prefix https://clients.xtablo.com/assets

      - name: Remove clients sourcemaps
        run: find apps/clients/dist -name '*.map' -delete

      - name: Build external
        run: pnpm --filter @xtablo/external build
        env:
          VITE_APP_VERSION: ${{ env.RELEASE_VERSION }}

      - name: Upload external sourcemaps
        run: pnpm exec datadog-ci sourcemaps upload apps/external/dist --service xtablo-external --release-version "$RELEASE_VERSION" --minified-path-prefix https://embed.xtablo.com/assets

      - name: Remove external sourcemaps
        run: find apps/external/dist -name '*.map' -delete
  • Step 4: Validate the workflow file structure

Run:

rg -n "datadog-ci sourcemaps upload|RELEASE_VERSION|find .*\\.map" .github/workflows/frontend-sourcemaps.yml

Expected: one upload and one cleanup step for each frontend app.

  • Step 5: Commit
git add package.json pnpm-lock.yaml .github/workflows/frontend-sourcemaps.yml
git commit -m "ci: upload frontend sourcemaps to Datadog"

Chunk 4: End-To-End Verification

Task 4: Prove builds emit hidden sourcemaps and cleanup works

Files:

  • No new source files

  • Step 1: Build each frontend locally with a release version

Run:

VITE_APP_VERSION=test-sha pnpm --filter @xtablo/main build:prod
VITE_APP_VERSION=test-sha pnpm --filter @xtablo/clients build:prod
VITE_APP_VERSION=test-sha pnpm --filter @xtablo/external build

Expected: each dist/ contains built assets and .map files, but the generated JS bundles do not include public sourceMappingURL comments.

  • Step 2: Verify .map files exist before cleanup

Run:

find apps/main/dist apps/clients/dist apps/external/dist -name '*.map' | sort

Expected: at least one sourcemap per app.

  • Step 3: Simulate CI cleanup locally

Run:

find apps/main/dist apps/clients/dist apps/external/dist -name '*.map' -delete
find apps/main/dist apps/clients/dist apps/external/dist -name '*.map' | sort

Expected: no output from the second command.

  • Step 4: Run the targeted regression suite

Run:

pnpm --filter @xtablo/main exec vitest run src/lib/rum.test.ts vite.config.test.ts --mode dev
pnpm --filter @xtablo/clients exec vitest run vite.config.test.ts --mode test
pnpm --filter @xtablo/external exec vitest run vite.config.test.ts --mode test
pnpm --filter @xtablo/main typecheck
pnpm --filter @xtablo/clients typecheck
pnpm --filter @xtablo/external typecheck

Expected: PASS.

  • Step 5: Commit
git status --short

Expected: clean worktree before opening the implementation PR.