From 017e3983d3659003c3919b81c4f70b1a6cdd94d0 Mon Sep 17 00:00:00 2001 From: Aymeric Mortemousque Date: Wed, 8 Apr 2026 17:14:43 +0200 Subject: [PATCH 01/12] =?UTF-8?q?=F0=9F=93=9D=20Add=20router=20integration?= =?UTF-8?q?=20pipeline=20design=20spec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design for a fully automated Claude Code skill pipeline that generates draft Browser SDK router integration PRs from framework public docs. Co-Authored-By: Claude Opus 4.6 (1M context) --- ...4-08-router-integration-pipeline-design.md | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-08-router-integration-pipeline-design.md diff --git a/docs/superpowers/specs/2026-04-08-router-integration-pipeline-design.md b/docs/superpowers/specs/2026-04-08-router-integration-pipeline-design.md new file mode 100644 index 0000000000..0bc062a54b --- /dev/null +++ b/docs/superpowers/specs/2026-04-08-router-integration-pipeline-design.md @@ -0,0 +1,227 @@ +# Router Integration Pipeline — Design Spec + +## Overview + +A fully automated Claude Code skill pipeline that generates draft Browser SDK router integration PRs from framework public documentation. The pipeline fetches framework router docs, analyzes concepts, maps them to existing SDK patterns, generates a complete package with tests, and opens a draft PR — with no human intervention until the PR is ready for review. + +## Invocation + +``` +/router:pipeline [...] +``` + +Example: +``` +/router:pipeline svelte https://svelte.dev/docs/kit/routing +``` + +## Architecture + +### Approach: Hybrid — Orchestrator + Stage Skills + +Six skills total: one orchestrator that chains five stage skills sequentially. Each stage produces a durable markdown artifact in `docs/integrations//`. Individual stage skills can be re-run independently. + +### Pipeline Flow + +``` +/router:pipeline + │ + ├─ /router:fetch-docs → 01-router-concepts.md + │ └─ EXIT if incompatible framework + │ + ├─ /router:analyze → 02-sdk-mapping.md + │ └─ EXIT if critical concepts unmapped + │ + ├─ /router:design → 03-design-decisions.md + │ + ├─ /router:generate → 04-generation-manifest.md + packages/rum-/ + │ + └─ /router:pr → Draft PR on GitHub +``` + +No human gates. Early exits produce an `EXIT.md` alongside whatever artifacts were written. + +## Artifacts + +All artifacts live in `docs/integrations//`. + +### Source Reference Convention + +Every factual claim in every artifact is inline-linked to its source — either an external doc URL or an internal file path with line range. Unsourced claims are explicitly marked as *inferred: \*. + +Example: +```markdown +Routes use [`:param` syntax](https://svelte.dev/docs/kit/routing#dynamic-parameters) +for dynamic segments, equivalent to Angular's +[`:id` in route config path](packages/rum-angular/src/domain/angularRouter/startAngularView.ts#L15-L28). +``` + +### `01-router-concepts.md` (Stage 1 output) + +Structured extraction from framework docs: + +- **Route definition format** — config object, file-based, decorators +- **Dynamic segment syntax** — `:id`, `[id]`, `{id}`, etc. +- **Catch-all/wildcard syntax** — `*`, `**`, `[...slug]`, etc. +- **Navigation lifecycle hooks** — which events fire and when +- **Navigation lifecycle timing** — specifically: where in the lifecycle do redirects resolve, and where do data fetches (loaders/resolvers) execute? The SDK wants to start a view as early as possible but after redirects and before both data fetches (loaders/resolvers) and component rendering, so that all resource loading and rendering work is attributed to the correct view. If no single hook satisfies all three constraints (after redirects, before data fetches, before render), document the trade-off and rank the options. Reference how existing integrations solve this: Angular uses [`GuardsCheckEnd`](packages/rum-angular/src/domain/angularRouter/provideDatadogRouter.ts) (after guards/redirects, before resolvers), Vue uses [`afterEach`](packages/rum-vue/src/domain/router/vueRouter.ts) (after everything), React uses [`subscribe`](packages/rum-react/src/domain/reactRouter/createRouter.ts) (after state change). +- **Route matching model** — nested vs flat, outlets, layouts +- **Programmatic navigation API** — how router exposes state +- **`compatible`** — boolean flag. `false` if the framework lacks a client-side route tree, dynamic segments, or navigation lifecycle events + +### `02-sdk-mapping.md` (Stage 2 output) + +Maps each framework concept to its SDK equivalent by reading reference implementations ([rum-angular](packages/rum-angular/), [rum-react](packages/rum-react/), [rum-vue](packages/rum-vue/)): + +- Navigation event → equivalent of Angular's [`GuardsCheckEnd`](packages/rum-angular/src/domain/angularRouter/provideDatadogRouter.ts), Vue's [`afterEach`](packages/rum-vue/src/domain/router/vueRouter.ts), React's [`subscribe`](packages/rum-react/src/domain/reactRouter/createRouter.ts) +- Dynamic segment syntax → `computeViewName` normalization strategy +- Catch-all handling → substitution approach +- Route tree shape → traversal algorithm +- Framework DI/plugin model → wrapping strategy (provider, hook, wrapper, plugin install) +- Peer dependencies needed +- Concepts marked `unmapped` if no SDK equivalent exists (with severity: `critical` or `minor`) + +### `03-design-decisions.md` (Stage 3 output) + +Design document covering: + +- Architecture decisions with rationale +- Public API surface (what the user imports and calls) +- File structure plan +- Test strategy and edge cases to cover +- Trade-offs and alternatives considered + +### `04-generation-manifest.md` (Stage 4 output) + +Listing of every generated file: + +- File path +- Purpose (one line) +- Which reference file it was modeled after (linked) +- Deviations from the reference pattern and why + +### `EXIT.md` (on early exit only) + +Written when the pipeline exits before completion: + +- Which stage failed or exited +- Reason with source links supporting the exit decision +- List of artifacts produced before exit + +## Stage Details + +### Stage 1: `/router:fetch-docs` + +**Input:** Framework name + doc URL(s) +**Output:** `01-router-concepts.md` + +Fetches and parses the provided URLs. Extracts the structured summary described above. Performs compatibility check — flags frameworks that lack standard routing concepts (no client-side route tree, no dynamic segments, no navigation events). Examples of incompatible frameworks: Shopify Hydrogen (server-only loaders), Salesforce Lightning (proprietary component model). + +### Stage 2: `/router:analyze` + +**Input:** `01-router-concepts.md` + reference implementations in `packages/rum-angular/`, `packages/rum-react/`, `packages/rum-vue/` +**Output:** `02-sdk-mapping.md` + +Reads reference implementations to understand the common contract: +- [Plugin interface](packages/rum-core/src/domain/plugins.ts) — `RumPlugin` with `onInit`/`onRumStart` +- [Public API](packages/rum-core/src/boot/rumPublicApi.ts) — `startView()` method +- `computeViewName()` implementations across all three reference packages +- Navigation event subscription patterns + +Maps each concept from Stage 1 to an SDK equivalent. Exits if critical concepts (navigation event, route tree access) have no mapping. + +### Stage 3: `/router:design` + +**Input:** `01-router-concepts.md` + `02-sdk-mapping.md` +**Output:** `03-design-decisions.md` + +Produces the design document. Synthesizes the mapping into concrete decisions: which files to create, what the public API looks like, how tests are structured. References both the framework docs and the SDK patterns that informed each decision. + +### Stage 4: `/router:generate` + +**Input:** All previous artifacts + reference implementations +**Output:** `packages/rum-/` + `04-generation-manifest.md` + +Generates the package structure: + +``` +packages/rum-/ +├── src/ +│ ├── entries/ +│ │ └── main.ts +│ ├── domain/ +│ │ ├── Plugin.ts +│ │ ├── Plugin.spec.ts +│ │ ├── Router/ +│ │ │ ├── startView.ts +│ │ │ ├── startView.spec.ts +│ │ │ ├── types.ts +│ │ │ └── .ts +│ │ └── error/ +│ │ ├── addError.ts +│ │ └── addError.spec.ts +│ └── test/ +│ └── initializePlugin.ts +├── package.json +├── tsconfig.json +└── README.md +``` + +**Code generation approach:** The agent reads reference implementations as examples (not templates). It uses `02-sdk-mapping.md` and `03-design-decisions.md` to determine the specific logic for: +- `.ts` — shaped by which navigation hook to subscribe to +- `computeViewName()` — shaped by dynamic segment and catch-all syntax +- Plugin exposure — shaped by framework DI/plugin model + +**Test generation:** Derived from two sources: +1. Common edge cases shared across all routers (from reference spec files): static paths, single/nested dynamic segments, empty/layout routes, catch-all/wildcard, duplicate navigation filtering, initial navigation +2. Framework-specific cases from `01-router-concepts.md` + +### Stage 5: `/router:pr` + +**Input:** Generated package + all artifacts +**Output:** Draft PR on GitHub + +- Creates branch: `/-router-integration` +- Two commits: + 1. `📝 Add router integration design docs` + 2. `✨ Add router integration package` +- Opens draft PR with body structured from `03-design-decisions.md`: + - Summary of what was generated + - Key design decisions + - Link to `docs/integrations//` for full artifact trail + +## Scope Boundaries + +### In scope +- Conventional SPA/SSR frontend frameworks with standard routing: route tree, dynamic params, navigation lifecycle (e.g. Svelte, Remix, Solid, Ember) +- Router integration (view tracking via `startView`) +- Error handler integration (following reference pattern) +- Unit tests for view name computation and navigation filtering + +### Out of scope +- Non-conventional frameworks (Shopify Hydrogen, Salesforce Lightning, etc.) — pipeline exits early +- E2E tests — not generated, left for human follow-up +- Publishing/release automation +- Modifications to existing packages or rum-core + +## Skill Location + +Skills live in `.claude/skills/router-integration/`: + +```text +.claude/skills/router-integration/ +├── pipeline.md # /router:pipeline — orchestrator +├── fetch-docs.md # /router:fetch-docs +├── analyze.md # /router:analyze +├── design.md # /router:design +├── generate.md # /router:generate +└── pr.md # /router:pr +``` + +## Stage Input Convention + +Each stage skill reads its inputs from the filesystem — no arguments are passed between skills except through the orchestrator's context: + +- **Orchestrator** receives `` and `` as arguments, writes them to `docs/integrations//00-pipeline-input.md` +- **Stage skills** read from `docs/integrations//` for previous artifacts and from `packages/rum-angular/`, `packages/rum-react/`, `packages/rum-vue/` for reference implementations +- **Framework name** is derived from the directory name From 9a9f06fe076c213a25c6b295ffcea72627cf57fb Mon Sep 17 00:00:00 2001 From: Aymeric Mortemousque Date: Wed, 8 Apr 2026 17:34:32 +0200 Subject: [PATCH 02/12] =?UTF-8?q?=F0=9F=93=9D=20Add=20router=20integration?= =?UTF-8?q?=20pipeline=20implementation=20plan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updated spec: skills use separate subdirectories (SKILL.md per dir) - Plan covers 7 tasks: orchestrator + 5 stage skills + validation - Each task has step-by-step instructions with exact file paths Co-Authored-By: Claude Opus 4.6 (1M context) --- .../2026-04-08-router-integration-pipeline.md | 802 ++++++++++++++++++ ...4-08-router-integration-pipeline-design.md | 16 +- 2 files changed, 810 insertions(+), 8 deletions(-) create mode 100644 docs/superpowers/plans/2026-04-08-router-integration-pipeline.md diff --git a/docs/superpowers/plans/2026-04-08-router-integration-pipeline.md b/docs/superpowers/plans/2026-04-08-router-integration-pipeline.md new file mode 100644 index 0000000000..91ded7b66f --- /dev/null +++ b/docs/superpowers/plans/2026-04-08-router-integration-pipeline.md @@ -0,0 +1,802 @@ +# Router Integration Pipeline — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a fully automated Claude Code skill pipeline that generates draft Browser SDK router integration PRs from framework public documentation. + +**Architecture:** Six Claude Code skills (one orchestrator + five stage skills), each in its own subdirectory under `.claude/skills/` with a `SKILL.md` entry point. Each stage reads/writes markdown artifacts in `docs/integrations//`. The orchestrator chains stages sequentially with early-exit conditions. + +**Tech Stack:** Claude Code skills (markdown), Bash (git/gh CLI), WebFetch (doc fetching), existing SDK reference implementations as examples. + +**Spec:** `docs/superpowers/specs/2026-04-08-router-integration-pipeline-design.md` + +--- + +## File Structure + +```text +.claude/skills/ +├── router-pipeline/SKILL.md # Orchestrator — /router:pipeline +├── router-fetch-docs/SKILL.md # Stage 1 — /router:fetch-docs +├── router-analyze/SKILL.md # Stage 2 — /router:analyze +├── router-design/SKILL.md # Stage 3 — /router:design +├── router-generate/SKILL.md # Stage 4 — /router:generate +└── router-pr/SKILL.md # Stage 5 — /router:pr +``` + +Runtime artifacts (created per invocation): + +```text +docs/integrations// +├── 00-pipeline-input.md +├── 01-router-concepts.md +├── 02-sdk-mapping.md +├── 03-design-decisions.md +├── 04-generation-manifest.md +└── EXIT.md # Only on early exit +``` + +--- + +### Task 1: Create the orchestrator skill + +**Files:** + +- Create: `.claude/skills/router-pipeline/SKILL.md` + +- [ ] **Step 1: Create the skill directory** + +```bash +mkdir -p .claude/skills/router-pipeline +``` + +- [ ] **Step 2: Write the orchestrator skill** + +Create `.claude/skills/router-pipeline/SKILL.md`: + +```markdown +--- +name: router:pipeline +description: Fully automated pipeline that generates a draft Browser SDK router integration PR from framework public docs. Usage: /router:pipeline [...] +--- + +# Router Integration Pipeline + +You are an orchestrator that chains five stage skills to generate a complete Browser SDK router integration package and draft PR. + +## Input + +Parse the arguments: first argument is the framework name (lowercase), remaining arguments are documentation URLs. + +Example: `/router:pipeline svelte https://svelte.dev/docs/kit/routing` + +## Step 1: Initialize + +Create the artifact directory and write the input file: + +```bash +mkdir -p docs/integrations/ +``` + +Write `docs/integrations//00-pipeline-input.md`: + +```markdown +# Pipeline Input + +**Framework:** +**Documentation URLs:** +- +- +**Initiated:** +``` + +## Step 2: Invoke /router:fetch-docs + +Use the Skill tool to invoke `router:fetch-docs`. + +After completion, read `docs/integrations//01-router-concepts.md` and check the `compatible` field. + +If `compatible: false`, write `docs/integrations//EXIT.md` with: +- Stage: fetch-docs +- Reason: the incompatibility reason from 01-router-concepts.md +- Artifacts produced: 00-pipeline-input.md, 01-router-concepts.md + +Then stop and report the exit to the user. + +## Step 3: Invoke /router:analyze + +Use the Skill tool to invoke `router:analyze`. + +After completion, read `docs/integrations//02-sdk-mapping.md` and check for any concept marked `unmapped` with severity `critical`. + +If critical unmapped concepts exist, write `EXIT.md` with: +- Stage: analyze +- Reason: which critical concepts could not be mapped and why +- Artifacts produced: 00-pipeline-input.md, 01-router-concepts.md, 02-sdk-mapping.md + +Then stop and report the exit to the user. + +## Step 4: Invoke /router:design + +Use the Skill tool to invoke `router:design`. + +## Step 5: Invoke /router:generate + +Use the Skill tool to invoke `router:generate`. + +## Step 6: Invoke /router:pr + +Use the Skill tool to invoke `router:pr`. + +## Error Handling + +If any stage fails (fetch timeout, parse error, tool failure), write `EXIT.md` with: +- Stage: which stage failed +- Reason: error details +- Artifacts produced: list of files written before failure + +All artifacts written before the failure are preserved. Stop and report the failure to the user. +``` + +- [ ] **Step 3: Verify the skill is discoverable** + +```bash +head -5 .claude/skills/router-pipeline/SKILL.md +``` + +Expected: the frontmatter with `name: router:pipeline`. + +- [ ] **Step 4: Commit** + +```bash +git add .claude/skills/router-pipeline/SKILL.md +git commit -m "✨ Add router:pipeline orchestrator skill" +``` + +--- + +### Task 2: Create the fetch-docs skill (Stage 1) + +**Files:** + +- Create: `.claude/skills/router-fetch-docs/SKILL.md` + +- [ ] **Step 1: Create the skill directory and write the skill** + +```bash +mkdir -p .claude/skills/router-fetch-docs +``` + +Create `.claude/skills/router-fetch-docs/SKILL.md`: + +```markdown +--- +name: router:fetch-docs +description: "Stage 1: Fetch framework router documentation and extract structured routing concepts. Reads input from docs/integrations//00-pipeline-input.md." +--- + +# Stage 1: Fetch Router Documentation + +## Context + +You are Stage 1 of the router integration pipeline. Your job is to fetch framework router documentation, extract structured routing concepts, and assess compatibility with the Browser SDK router integration model. + +## Input + +Read `docs/integrations//00-pipeline-input.md` to get the framework name and documentation URLs. Find the `` directory by listing `docs/integrations/` and finding the most recently created subdirectory. + +## Process + +### 1. Fetch Documentation + +Use the WebFetch tool to fetch each documentation URL. If a URL fails, note it and continue with remaining URLs. If all URLs fail, write EXIT.md and stop. + +### 2. Extract Router Concepts + +Analyze the fetched documentation and produce a structured summary covering these sections. Every factual claim MUST be inline-linked to the source URL and section where you found it. + +Unsourced claims must be marked as *inferred: *. + +#### Required Sections + +**Route Definition Format** +How routes are declared: config object, file-based routing, decorators, or other. Include code examples from the docs. + +**Dynamic Segment Syntax** +What syntax the framework uses for parameterized route segments (e.g. `:id`, `[id]`, `{id}`). Include examples. + +**Catch-All / Wildcard Syntax** +What syntax the framework uses for catch-all or wildcard routes (e.g. `*`, `**`, `[...slug]`, `/:pathMatch(.*)*`). Include examples. If the framework has no catch-all syntax, state that explicitly. + +**Navigation Lifecycle Hooks** +List every navigation lifecycle event/hook the framework exposes, in the order they fire. For each, describe: +- When it fires relative to other hooks +- What data is available at that point +- Whether it can cancel/redirect navigation + +**Navigation Lifecycle Timing** +This is the most critical section. Determine: +- Where in the lifecycle do **redirects** resolve? +- Where do **data fetches** (loaders, resolvers) execute? +- Where does **component rendering** begin? + +The SDK wants to start a view **as early as possible** but: +1. **After redirects** — so the view name reflects the final destination +2. **Before data fetches** — so loader/resolver network requests are attributed to the correct view +3. **Before component rendering** — so rendering work is attributed to the correct view + +Identify the ideal hook point. If no single hook satisfies all three constraints, document the trade-off and rank the available options from best to worst, explaining what is lost with each. + +Reference how existing integrations solve this: +- Angular uses [`GuardsCheckEnd`](packages/rum-angular/src/domain/angularRouter/provideDatadogRouter.ts) (after guards/redirects, before resolvers) +- Vue uses [`afterEach`](packages/rum-vue/src/domain/router/vueRouter.ts) (after everything including render) +- React uses [`subscribe`](packages/rum-react/src/domain/reactRouter/createRouter.ts) (after state change) + +**Route Matching Model** +How the framework matches URLs to routes: nested vs flat, layout routes, named outlets/slots, parallel routes. + +**Programmatic Navigation API** +How the router exposes current route state and navigation methods. What objects/hooks are available to read the current route, its params, and matched route records. + +### 3. Compatibility Assessment + +At the end of the document, include a `## Compatibility` section with: + +- `compatible: true` or `compatible: false` +- If false, a `reason:` field explaining why + +A framework is **incompatible** if it lacks: +- A client-side route tree or route matching mechanism +- Dynamic segment parameters +- Navigation lifecycle events that can be hooked into +- Examples: Shopify Hydrogen (server-only loaders), Salesforce Lightning (proprietary component model) + +A framework is **compatible** even if: +- It uses file-based routing (as long as there's a runtime route representation) +- Some hooks fire later than ideal (trade-off is documented, not a blocker) + +## Output + +Write the result to `docs/integrations//01-router-concepts.md`. +``` + +- [ ] **Step 2: Verify the skill file** + +```bash +head -5 .claude/skills/router-fetch-docs/SKILL.md +``` + +Expected: frontmatter with `name: router:fetch-docs`. + +- [ ] **Step 3: Commit** + +```bash +git add .claude/skills/router-fetch-docs/SKILL.md +git commit -m "✨ Add router:fetch-docs skill (Stage 1)" +``` + +--- + +### Task 3: Create the analyze skill (Stage 2) + +**Files:** + +- Create: `.claude/skills/router-analyze/SKILL.md` + +- [ ] **Step 1: Create the skill directory and write the skill** + +```bash +mkdir -p .claude/skills/router-analyze +``` + +Create `.claude/skills/router-analyze/SKILL.md`: + +```markdown +--- +name: router:analyze +description: "Stage 2: Map framework router concepts to existing SDK patterns. Reads 01-router-concepts.md and reference implementations." +--- + +# Stage 2: Analyze and Map to SDK Patterns + +## Context + +You are Stage 2 of the router integration pipeline. Your job is to read the router concepts extracted in Stage 1 and map each one to the equivalent pattern in the existing Browser SDK framework integrations. + +## Input + +1. Read `docs/integrations//01-router-concepts.md` for the framework's router concepts +2. Read the reference implementations to understand the SDK contract: + - Plugin interface: `packages/rum-core/src/domain/plugins.ts` + - Public API: `packages/rum-core/src/boot/rumPublicApi.ts` + - Angular router: `packages/rum-angular/src/domain/angularRouter/` (all files) + - React router: `packages/rum-react/src/domain/reactRouter/` (all files) + - Vue router: `packages/rum-vue/src/domain/router/` (all files) + +Find the `` directory by listing `docs/integrations/`. + +## Process + +For each concept in `01-router-concepts.md`, find the closest equivalent in the reference implementations. Every mapping MUST include inline links to both: +- The framework doc source (from 01-router-concepts.md links) +- The specific file and line range in the reference implementation + +### Required Mappings + +**Navigation Event Mapping** +Which framework hook/event to subscribe to, and which reference implementation it's most similar to. Justify the choice based on the lifecycle timing analysis from Stage 1 (after redirects, before data fetches, before render). If the chosen hook involves trade-offs, document them here with links to the Stage 1 analysis. + +**View Name Computation** +How to build the `computeViewName()` function: +- How to access the matched route records after navigation +- How dynamic segments appear in the route definition (and whether they need normalization) +- How catch-all/wildcard routes should be substituted with actual path segments +- Link to the most similar existing `computeViewName` implementation and note differences + +**Wrapping Strategy** +How the integration hooks into the framework: +- Angular: [`ENVIRONMENT_INITIALIZER` provider](packages/rum-angular/src/domain/angularRouter/provideDatadogRouter.ts) +- React: [wrapper around `createRouter`](packages/rum-react/src/domain/reactRouter/createRouter.ts) and [`useRoutes` hook](packages/rum-react/src/domain/reactRouter/useRoutes.ts) +- Vue: [wrapper around `createRouter`](packages/rum-vue/src/domain/router/vueRouter.ts) + +Determine which pattern fits this framework and why. + +**Type Strategy** +Whether to define minimal local types (like Angular's [`RouteSnapshot`](packages/rum-angular/src/domain/angularRouter/types.ts)) to avoid runtime framework imports, or import types directly from the framework package. + +**Plugin Configuration** +How the plugin will be configured. All reference implementations use the same pattern: +- [`VuePluginConfiguration`](packages/rum-vue/src/domain/vuePlugin.ts) with `router?: boolean` +- `onInit` sets `trackViewsManually = true` when `router: true` + +**Peer Dependencies** +Which framework packages are required as peer dependencies, with version ranges. + +**Navigation Filtering** +How to handle: +- Failed navigations (guards blocking, cancellations) +- Duplicate navigations (same path) +- Query-only changes +- Initial navigation + +Reference the filtering logic in existing implementations: +- Vue: [lines 15-22 of vueRouter.ts](packages/rum-vue/src/domain/router/vueRouter.ts) +- React: subscribe callback in [createRouter.ts](packages/rum-react/src/domain/reactRouter/createRouter.ts) + +### Unmapped Concepts + +For any framework concept that has no SDK equivalent, create a section: + +```markdown +### Unmapped: + +**Severity:** critical | minor +**Reason:** +**Impact:** +``` + +- `critical`: the integration cannot work without this (e.g. no way to get route matches) +- `minor`: the integration works but this feature isn't supported (e.g. named outlets not tracked) + +## Output + +Write the result to `docs/integrations//02-sdk-mapping.md`. +``` + +- [ ] **Step 2: Commit** + +```bash +git add .claude/skills/router-analyze/SKILL.md +git commit -m "✨ Add router:analyze skill (Stage 2)" +``` + +--- + +### Task 4: Create the design skill (Stage 3) + +**Files:** + +- Create: `.claude/skills/router-design/SKILL.md` + +- [ ] **Step 1: Create the skill directory and write the skill** + +```bash +mkdir -p .claude/skills/router-design +``` + +Create `.claude/skills/router-design/SKILL.md`: + +```markdown +--- +name: router:design +description: "Stage 3: Produce design decisions document from router concepts and SDK mapping. Reads 01 and 02 artifacts." +--- + +# Stage 3: Design Decisions + +## Context + +You are Stage 3 of the router integration pipeline. Your job is to synthesize the router concepts (Stage 1) and SDK mapping (Stage 2) into a concrete design document that will guide code generation. + +## Input + +1. Read `docs/integrations//01-router-concepts.md` +2. Read `docs/integrations//02-sdk-mapping.md` +3. Read reference implementations for structural patterns: + - Angular entry point: `packages/rum-angular/src/entries/main.ts` + - Vue entry point: `packages/rum-vue/src/entries/main.ts` + - React entry point: `packages/rum-react/src/entries/main.ts` + - Vue package.json: `packages/rum-vue/package.json` + - Angular package.json: `packages/rum-angular/package.json` + +Find the `` directory by listing `docs/integrations/`. + +## Process + +Produce a design document with these sections. Every decision MUST link back to the concept or mapping that informed it. + +### Required Sections + +**Architecture Overview** +2-3 sentences describing the overall approach. Which reference implementation is closest and why. + +**Public API** +Exactly what the user imports and calls. Show the complete setup code example: +```typescript +// What the user writes in their app +import { ... } from '@datadog/browser-rum-' +``` + +**File Structure** +The exact file tree for `packages/rum-/` with one-line descriptions per file. Follow the convention from reference packages: +- `src/entries/main.ts` — public exports +- `src/domain/Plugin.ts` — plugin + subscriber pattern +- `src/domain/Router/` — router integration files +- `src/domain/error/` — error handling files +- `src/test/` — test helpers +- `package.json`, `tsconfig.json`, `README.md` + +**Navigation Hook Decision** +Which hook to use, with the full trade-off analysis from Stage 1 and the mapping from Stage 2. This is the most important architectural decision — make it explicit. + +**View Name Algorithm** +Pseudocode or step-by-step description of how `computeViewName()` will work for this framework. Cover: +- Normal routes +- Dynamic segments +- Nested routes +- Catch-all/wildcard routes +- Edge cases specific to this framework + +**Navigation Filtering** +How failed, duplicate, query-only, and initial navigations are handled. + +**Error Handler Integration** +How the framework's error handling mechanism will be integrated, following the pattern from reference implementations: +- Angular: [`provideDatadogErrorHandler`](packages/rum-angular/src/domain/error/provideDatadogErrorHandler.ts) +- Vue: [`addVueError`](packages/rum-vue/src/domain/error/addVueError.ts) +- React: [`ErrorBoundary`](packages/rum-react/src/domain/error/errorBoundary.ts) + +**Test Strategy** +List every test case to implement, grouped by file: +- `Plugin.spec.ts`: plugin structure, subscriber callbacks, telemetry, trackViewsManually +- `startView.spec.ts`: all view name computation cases (static, dynamic, nested, catch-all, edge cases) +- Router integration spec: navigation event handling, filtering, deduplication +- Error handler spec: error reporting, context merging + +**Trade-offs and Alternatives** +Document any decisions where multiple valid approaches existed. For each, state what was chosen, what was rejected, and why. + +## Output + +Write the result to `docs/integrations//03-design-decisions.md`. +``` + +- [ ] **Step 2: Commit** + +```bash +git add .claude/skills/router-design/SKILL.md +git commit -m "✨ Add router:design skill (Stage 3)" +``` + +--- + +### Task 5: Create the generate skill (Stage 4) + +**Files:** + +- Create: `.claude/skills/router-generate/SKILL.md` + +- [ ] **Step 1: Create the skill directory and write the skill** + +```bash +mkdir -p .claude/skills/router-generate +``` + +Create `.claude/skills/router-generate/SKILL.md`: + +```markdown +--- +name: router:generate +description: "Stage 4: Generate the complete router integration package from design artifacts and reference implementations." +--- + +# Stage 4: Generate Package + +## Context + +You are Stage 4 of the router integration pipeline. Your job is to generate a complete, working Browser SDK router integration package based on the design decisions from Stage 3 and the patterns from reference implementations. + +## Input + +1. Read `docs/integrations//01-router-concepts.md` +2. Read `docs/integrations//02-sdk-mapping.md` +3. Read `docs/integrations//03-design-decisions.md` — this is your primary guide +4. Read reference implementations for code patterns (read the actual source files, not summaries): + - The reference implementation identified as "closest" in the design doc + - Plugin: e.g. `packages/rum-vue/src/domain/vuePlugin.ts` + - Router: e.g. `packages/rum-vue/src/domain/router/` + - Error: e.g. `packages/rum-vue/src/domain/error/` + - Tests: corresponding `.spec.ts` files + - Test helper: e.g. `packages/rum-vue/src/test/initializeVuePlugin.ts` + - Package config: e.g. `packages/rum-vue/package.json`, `packages/rum-vue/tsconfig.json` + - Entry point: e.g. `packages/rum-vue/src/entries/main.ts` + +Find the `` directory by listing `docs/integrations/`. + +## Process + +### 1. Generate Each File + +Follow the file structure defined in `03-design-decisions.md`. For each file: + +1. Read the corresponding reference implementation file +2. Adapt it to the target framework using the mappings from `02-sdk-mapping.md` and decisions from `03-design-decisions.md` +3. Write the file using the Write tool + +**Code conventions** (from `AGENTS.md`): +- Use **camelCase** for all internal variables and object properties +- Conversion to snake_case happens at serialization boundary only +- Use TypeScript type narrowing over runtime assertions +- Follow existing patterns exactly — match import style, export style, comment style + +**Plugin file** (`Plugin.ts`): +- Follow the exact pattern from the reference: global state, subscriber arrays, `onRumInit`/`onRumStart` exports, `resetPlugin` for tests +- Plugin name must be the framework name in lowercase +- Configuration interface with `router?: boolean` +- `getConfigurationTelemetry` returning `{ router: !!configuration.router }` + +**Router integration files**: +- `types.ts`: minimal interface for route-related types, avoiding runtime framework imports where possible +- `startView.ts`: `computeViewName()` + `startRouterView()` calling `onRumInit` +- Integration point file: the framework-specific wrapper/provider/hook + +**Error files**: +- Follow the reference pattern: capture error + stack + timing, defer via `onRumStart` +- Include `dd_context` merging from original error + +**Test files**: +- Follow Jasmine conventions: `describe`/`it` blocks +- Use `registerCleanupTask()` for cleanup, NOT `afterEach()` +- Use the test helper for plugin initialization +- Cover every test case listed in `03-design-decisions.md` + +**package.json**: +- Follow the reference `package.json` structure exactly +- Set version to match current monorepo version (read from a reference package) +- Set correct peer dependencies from `02-sdk-mapping.md` +- Include both ESM and CJS entry points + +**tsconfig.json**: +- Copy from nearest reference package, adjust paths + +**README.md**: +- Follow the reference README structure +- Include setup instructions matching the public API from `03-design-decisions.md` + +### 2. Write Generation Manifest + +After all files are generated, write `docs/integrations//04-generation-manifest.md`: + +```markdown +# Generation Manifest + +## Generated Files + +| File | Purpose | Modeled After | Deviations | +|------|---------|---------------|------------| +| `packages/rum-/src/domain/Plugin.ts` | Plugin + subscriber pattern | [`vuePlugin.ts`](packages/rum-vue/src/domain/vuePlugin.ts) | None | +| ... | ... | ... | ... | +``` + +For each file, link to the reference file it was modeled after. If there are deviations from the reference pattern, explain why. + +## Output + +- Generated package in `packages/rum-/` +- Manifest at `docs/integrations//04-generation-manifest.md` +``` + +- [ ] **Step 2: Commit** + +```bash +git add .claude/skills/router-generate/SKILL.md +git commit -m "✨ Add router:generate skill (Stage 4)" +``` + +--- + +### Task 6: Create the PR skill (Stage 5) + +**Files:** + +- Create: `.claude/skills/router-pr/SKILL.md` + +- [ ] **Step 1: Create the skill directory and write the skill** + +```bash +mkdir -p .claude/skills/router-pr +``` + +Create `.claude/skills/router-pr/SKILL.md`: + +```markdown +--- +name: router:pr +description: "Stage 5: Create a git branch, commit artifacts and generated code, and open a draft PR on GitHub." +--- + +# Stage 5: Create Draft PR + +## Context + +You are Stage 5 of the router integration pipeline. Your job is to create a branch, commit all generated artifacts and code, and open a draft PR on GitHub. + +## Input + +1. Read `docs/integrations//03-design-decisions.md` for PR body content +2. Read `docs/integrations//04-generation-manifest.md` for the list of generated files +3. Determine the current git user name from `git config user.name` (for branch naming) + +Find the `` directory by listing `docs/integrations/`. + +## Process + +### 1. Create Branch + +```bash +# Get the current user's branch prefix +USER=$(git config user.name | tr ' ' '.' | tr '[:upper:]' '[:lower:]') +BRANCH="${USER}/-router-integration" + +git checkout -b "$BRANCH" +``` + +### 2. Commit Artifacts + +First commit: design documentation only. + +```bash +git add docs/integrations// +git commit -m "📝 Add router integration design docs + +Generated by the router integration pipeline. +Artifacts: pipeline input, router concepts, SDK mapping, design decisions, generation manifest." +``` + +### 3. Commit Generated Package + +Second commit: the generated package. + +```bash +git add packages/rum-/ +git commit -m "✨ Add router integration package + +Auto-generated from framework documentation using the router integration pipeline. +See docs/integrations// for design artifacts and decision rationale." +``` + +### 4. Push and Create Draft PR + +```bash +git push -u origin "$BRANCH" +``` + +Then use `gh pr create` with this structure: + +```bash +gh pr create --draft --title "✨ Add router integration" --body "$(cat <<'PREOF' +## Summary + +Auto-generated router integration for using the router integration pipeline. + +- Router view tracking via `startView()` on route changes +- Error handler integration +- Unit tests for view name computation and navigation filtering + +## Design Artifacts + +Full design trail at `docs/integrations//`: +- `01-router-concepts.md` — extracted routing concepts from framework docs +- `02-sdk-mapping.md` — mapping to existing SDK patterns +- `03-design-decisions.md` — architecture and API decisions +- `04-generation-manifest.md` — list of generated files with lineage + +## Key Decisions + + + +## Test plan + +- [ ] Review design artifacts in `docs/integrations//` +- [ ] Review generated code against reference implementations +- [ ] Run `yarn typecheck` to verify type correctness +- [ ] Run `yarn test:unit --spec packages/rum-/` to verify tests pass +- [ ] Manual review of `computeViewName()` edge cases + +🤖 Generated with the router integration pipeline +PREOF +)" +``` + +Replace `` with the actual framework name. Replace the key decisions placeholder with actual content from `03-design-decisions.md`. + +## Output + +Report the PR URL to the user. +``` + +- [ ] **Step 2: Commit** + +```bash +git add .claude/skills/router-pr/SKILL.md +git commit -m "✨ Add router:pr skill (Stage 5)" +``` + +--- + +### Task 7: End-to-end validation + +**Files:** + +- No new files — validation only + +- [ ] **Step 1: Verify all six skill directories exist** + +```bash +ls -d .claude/skills/router-*/ +``` + +Expected: `router-pipeline/`, `router-fetch-docs/`, `router-analyze/`, `router-design/`, `router-generate/`, `router-pr/` + +- [ ] **Step 2: Verify each skill has correct frontmatter** + +```bash +for f in .claude/skills/router-*/SKILL.md; do + echo "=== $f ===" + head -4 "$f" + echo +done +``` + +Expected: each file has `---` delimiters with `name:` and `description:` fields. Names should be: `router:pipeline`, `router:fetch-docs`, `router:analyze`, `router:design`, `router:generate`, `router:pr`. + +- [ ] **Step 3: Verify the orchestrator references all stage skills** + +```bash +grep -n "router:" .claude/skills/router-pipeline/SKILL.md +``` + +Expected: references to all five stage skill names (`router:fetch-docs`, `router:analyze`, `router:design`, `router:generate`, `router:pr`). + +- [ ] **Step 4: Commit if any uncommitted changes remain** + +```bash +git status +``` + +If there are uncommitted changes: + +```bash +git add .claude/skills/router-*/SKILL.md +git commit -m "🔧 Final adjustments to router integration pipeline skills" +``` diff --git a/docs/superpowers/specs/2026-04-08-router-integration-pipeline-design.md b/docs/superpowers/specs/2026-04-08-router-integration-pipeline-design.md index 0bc062a54b..4f8405c689 100644 --- a/docs/superpowers/specs/2026-04-08-router-integration-pipeline-design.md +++ b/docs/superpowers/specs/2026-04-08-router-integration-pipeline-design.md @@ -206,16 +206,16 @@ packages/rum-/ ## Skill Location -Skills live in `.claude/skills/router-integration/`: +Each skill lives in its own subdirectory under `.claude/skills/` with a `SKILL.md` entry point (Claude Code requires one `SKILL.md` per subdirectory): ```text -.claude/skills/router-integration/ -├── pipeline.md # /router:pipeline — orchestrator -├── fetch-docs.md # /router:fetch-docs -├── analyze.md # /router:analyze -├── design.md # /router:design -├── generate.md # /router:generate -└── pr.md # /router:pr +.claude/skills/ +├── router-pipeline/SKILL.md # /router:pipeline — orchestrator +├── router-fetch-docs/SKILL.md # /router:fetch-docs +├── router-analyze/SKILL.md # /router:analyze +├── router-design/SKILL.md # /router:design +├── router-generate/SKILL.md # /router:generate +└── router-pr/SKILL.md # /router:pr ``` ## Stage Input Convention From 4849cc3933b7a029ec53d66088060bf880403599 Mon Sep 17 00:00:00 2001 From: Aymeric Mortemousque Date: Thu, 9 Apr 2026 11:17:07 +0200 Subject: [PATCH 03/12] =?UTF-8?q?=E2=9C=A8=20Add=20router=20integration=20?= =?UTF-8?q?pipeline=20skills?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six Claude Code skills forming an automated pipeline: - router:pipeline — orchestrator chaining all stages - router:fetch-docs — Stage 1: fetch & extract router concepts - router:analyze — Stage 2: map to SDK patterns - router:design — Stage 3: produce design decisions - router:generate — Stage 4: generate package code - router:pr — Stage 5: create draft PR Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/router-analyze/SKILL.md | 89 +++++++++++++++++++ .claude/skills/router-design/SKILL.md | 81 +++++++++++++++++ .claude/skills/router-fetch-docs/SKILL.md | 94 ++++++++++++++++++++ .claude/skills/router-generate/SKILL.md | 94 ++++++++++++++++++++ .claude/skills/router-pipeline/SKILL.md | 81 +++++++++++++++++ .claude/skills/router-pr/SKILL.md | 103 ++++++++++++++++++++++ 6 files changed, 542 insertions(+) create mode 100644 .claude/skills/router-analyze/SKILL.md create mode 100644 .claude/skills/router-design/SKILL.md create mode 100644 .claude/skills/router-fetch-docs/SKILL.md create mode 100644 .claude/skills/router-generate/SKILL.md create mode 100644 .claude/skills/router-pipeline/SKILL.md create mode 100644 .claude/skills/router-pr/SKILL.md diff --git a/.claude/skills/router-analyze/SKILL.md b/.claude/skills/router-analyze/SKILL.md new file mode 100644 index 0000000000..9ccff031ac --- /dev/null +++ b/.claude/skills/router-analyze/SKILL.md @@ -0,0 +1,89 @@ +--- +name: router:analyze +description: "Stage 2: Map framework router concepts to existing SDK patterns. Reads 01-router-concepts.md and reference implementations." +--- + +# Stage 2: Analyze and Map to SDK Patterns + +## Context + +You are Stage 2 of the router integration pipeline. Your job is to read the router concepts extracted in Stage 1 and map each one to the equivalent pattern in the existing Browser SDK framework integrations. + +## Input + +1. Read `docs/integrations//01-router-concepts.md` for the framework's router concepts +2. Read the reference implementations to understand the SDK contract: + - Plugin interface: `packages/rum-core/src/domain/plugins.ts` + - Public API: `packages/rum-core/src/boot/rumPublicApi.ts` + - Angular router: `packages/rum-angular/src/domain/angularRouter/` (all files) + - React router: `packages/rum-react/src/domain/reactRouter/` (all files) + - Vue router: `packages/rum-vue/src/domain/router/` (all files) + +Find the `` directory by listing `docs/integrations/`. + +## Process + +For each concept in `01-router-concepts.md`, find the closest equivalent in the reference implementations. Every mapping MUST include inline links to both: +- The framework doc source (from 01-router-concepts.md links) +- The specific file and line range in the reference implementation + +### Required Mappings + +**Navigation Event Mapping** +Which framework hook/event to subscribe to, and which reference implementation it's most similar to. Justify the choice based on the lifecycle timing analysis from Stage 1 (after redirects, before data fetches, before render). If the chosen hook involves trade-offs, document them here with links to the Stage 1 analysis. + +**View Name Computation** +How to build the `computeViewName()` function: +- How to access the matched route records after navigation +- How dynamic segments appear in the route definition (and whether they need normalization) +- How catch-all/wildcard routes should be substituted with actual path segments +- Link to the most similar existing `computeViewName` implementation and note differences + +**Wrapping Strategy** +How the integration hooks into the framework: +- Angular: [`ENVIRONMENT_INITIALIZER` provider](packages/rum-angular/src/domain/angularRouter/provideDatadogRouter.ts) +- React: [wrapper around `createRouter`](packages/rum-react/src/domain/reactRouter/createRouter.ts) and [`useRoutes` hook](packages/rum-react/src/domain/reactRouter/useRoutes.ts) +- Vue: [wrapper around `createRouter`](packages/rum-vue/src/domain/router/vueRouter.ts) + +Determine which pattern fits this framework and why. + +**Type Strategy** +Whether to define minimal local types (like Angular's [`RouteSnapshot`](packages/rum-angular/src/domain/angularRouter/types.ts)) to avoid runtime framework imports, or import types directly from the framework package. + +**Plugin Configuration** +How the plugin will be configured. All reference implementations use the same pattern: +- [`VuePluginConfiguration`](packages/rum-vue/src/domain/vuePlugin.ts) with `router?: boolean` +- `onInit` sets `trackViewsManually = true` when `router: true` + +**Peer Dependencies** +Which framework packages are required as peer dependencies, with version ranges. + +**Navigation Filtering** +How to handle: +- Failed navigations (guards blocking, cancellations) +- Duplicate navigations (same path) +- Query-only changes +- Initial navigation + +Reference the filtering logic in existing implementations: +- Vue: [lines 15-22 of vueRouter.ts](packages/rum-vue/src/domain/router/vueRouter.ts) +- React: subscribe callback in [createRouter.ts](packages/rum-react/src/domain/reactRouter/createRouter.ts) + +### Unmapped Concepts + +For any framework concept that has no SDK equivalent, create a section: + +```markdown +### Unmapped: + +**Severity:** critical | minor +**Reason:** +**Impact:** +``` + +- `critical`: the integration cannot work without this (e.g. no way to get route matches) +- `minor`: the integration works but this feature isn't supported (e.g. named outlets not tracked) + +## Output + +Write the result to `docs/integrations//02-sdk-mapping.md`. diff --git a/.claude/skills/router-design/SKILL.md b/.claude/skills/router-design/SKILL.md new file mode 100644 index 0000000000..a382701c65 --- /dev/null +++ b/.claude/skills/router-design/SKILL.md @@ -0,0 +1,81 @@ +--- +name: router:design +description: "Stage 3: Produce design decisions document from router concepts and SDK mapping. Reads 01 and 02 artifacts." +--- + +# Stage 3: Design Decisions + +## Context + +You are Stage 3 of the router integration pipeline. Your job is to synthesize the router concepts (Stage 1) and SDK mapping (Stage 2) into a concrete design document that will guide code generation. + +## Input + +1. Read `docs/integrations//01-router-concepts.md` +2. Read `docs/integrations//02-sdk-mapping.md` +3. Read reference implementations for structural patterns: + - Angular entry point: `packages/rum-angular/src/entries/main.ts` + - Vue entry point: `packages/rum-vue/src/entries/main.ts` + - React entry point: `packages/rum-react/src/entries/main.ts` + - Vue package.json: `packages/rum-vue/package.json` + - Angular package.json: `packages/rum-angular/package.json` + +Find the `` directory by listing `docs/integrations/`. + +## Process + +Produce a design document with these sections. Every decision MUST link back to the concept or mapping that informed it. + +### Required Sections + +**Architecture Overview** +2-3 sentences describing the overall approach. Which reference implementation is closest and why. + +**Public API** +Exactly what the user imports and calls. Show the complete setup code example: +```typescript +// What the user writes in their app +import { ... } from '@datadog/browser-rum-' +``` + +**File Structure** +The exact file tree for `packages/rum-/` with one-line descriptions per file. Follow the convention from reference packages: +- `src/entries/main.ts` — public exports +- `src/domain/Plugin.ts` — plugin + subscriber pattern +- `src/domain/Router/` — router integration files +- `src/test/` — test helpers +- `package.json`, `tsconfig.json`, `README.md` + +**Navigation Hook Decision** +Which hook to use, with the full trade-off analysis from Stage 1 and the mapping from Stage 2. This is the most important architectural decision — make it explicit. + +**View Name Algorithm** +Pseudocode or step-by-step description of how `computeViewName()` will work for this framework. Cover: +- Normal routes +- Dynamic segments +- Nested routes +- Catch-all/wildcard routes +- Edge cases specific to this framework + +IMPORTANT: The navigation hook choice directly affects what data is available to `computeViewName()`. Different hooks may expose different route objects, matched route arrays, or URL representations. If the framework has multiple candidate hooks (from the Navigation Hook Decision section), show how the view name computation differs for each option: +- What route data each hook provides (e.g. matched route records vs. raw URL vs. route config) +- How that changes the `computeViewName()` implementation +- Whether one hook gives better data for view name computation (e.g. access to parameterized route patterns vs. only resolved URLs) + +This analysis should reinforce or challenge the hook choice made above — if a hook that fires later provides significantly better route data, that trade-off must be documented. + +**Navigation Filtering** +How failed, duplicate, query-only, and initial navigations are handled. + +**Test Strategy** +List every test case to implement, grouped by file: +- `Plugin.spec.ts`: plugin structure, subscriber callbacks, telemetry, trackViewsManually +- `startView.spec.ts`: all view name computation cases (static, dynamic, nested, catch-all, edge cases) +- Router integration spec: navigation event handling, filtering, deduplication + +**Trade-offs and Alternatives** +Document any decisions where multiple valid approaches existed. For each, state what was chosen, what was rejected, and why. + +## Output + +Write the result to `docs/integrations//03-design-decisions.md`. diff --git a/.claude/skills/router-fetch-docs/SKILL.md b/.claude/skills/router-fetch-docs/SKILL.md new file mode 100644 index 0000000000..bc51d4b93a --- /dev/null +++ b/.claude/skills/router-fetch-docs/SKILL.md @@ -0,0 +1,94 @@ +--- +name: router:fetch-docs +description: "Stage 1: Fetch framework router documentation and extract structured routing concepts. Reads input from docs/integrations//00-pipeline-input.md." +--- + +# Stage 1: Fetch Router Documentation + +## Context + +You are Stage 1 of the router integration pipeline. Your job is to fetch framework router documentation, extract structured routing concepts, and assess compatibility with the Browser SDK router integration model. + +## Input + +Read `docs/integrations//00-pipeline-input.md` to get the framework name and documentation URLs. Find the `` directory by listing `docs/integrations/` and finding the most recently created subdirectory. + +## Process + +### 1. Fetch Documentation + +Before fetching the provided URLs directly, try to find LLM-friendly versions of the docs: + +1. **Check for `llms.txt`** — Fetch `/llms.txt` (e.g. `https://svelte.dev/llms.txt`). This file indexes markdown documentation pages designed for LLM consumption. If it exists, use it to navigate to the relevant routing pages. +2. **Try `.md` suffix** — For each doc URL, try appending `.md` to the path (e.g. `https://svelte.dev/docs/kit/routing.md`). Many doc sites serve a raw markdown version this way, which is much easier to parse accurately. +3. **Fall back to HTML** — If neither LLM-friendly format is available, fetch the original URLs. + +Use the WebFetch tool for all fetches. If a URL fails, note it and continue with remaining URLs. If all URLs fail, write EXIT.md and stop. + +### 2. Extract Router Concepts + +Analyze the fetched documentation and produce a structured summary covering these sections. Every factual claim MUST be inline-linked to the source URL and section where you found it. + +Unsourced claims must be marked as *inferred: \*. + +#### Required Sections + +**Route Definition Format** +How routes are declared: config object, file-based routing, decorators, or other. Include code examples from the docs. + +**Dynamic Segment Syntax** +What syntax the framework uses for parameterized route segments (e.g. `:id`, `[id]`, `{id}`). Include examples. + +**Catch-All / Wildcard Syntax** +What syntax the framework uses for catch-all or wildcard routes (e.g. `*`, `**`, `[...slug]`, `/:pathMatch(.*)*`). Include examples. If the framework has no catch-all syntax, state that explicitly. + +**Navigation Lifecycle Hooks** +List every navigation lifecycle event/hook the framework exposes, in the order they fire. For each, describe: +- When it fires relative to other hooks +- What data is available at that point +- Whether it can cancel/redirect navigation + +**Navigation Lifecycle Timing** +This is the most critical section. Determine: +- Where in the lifecycle do **redirects** resolve? +- Where do **data fetches** (loaders, resolvers) execute? +- Where does **component rendering** begin? + +The SDK wants to start a view **as early as possible** but: +1. **After redirects** — so the view name reflects the final destination +2. **Before data fetches** — so loader/resolver network requests are attributed to the correct view +3. **Before component rendering** — so rendering work is attributed to the correct view + +Identify the ideal hook point. If no single hook satisfies all three constraints, document the trade-off and rank the available options from best to worst, explaining what is lost with each. + +Reference how existing integrations solve this: +- Angular uses [`GuardsCheckEnd`](packages/rum-angular/src/domain/angularRouter/provideDatadogRouter.ts) (after guards/redirects, before resolvers) +- Vue uses [`afterEach`](packages/rum-vue/src/domain/router/vueRouter.ts) (after everything including render) +- React uses [`subscribe`](packages/rum-react/src/domain/reactRouter/createRouter.ts) (after state change) + +**Route Matching Model** +How the framework matches URLs to routes: nested vs flat, layout routes, named outlets/slots, parallel routes. + +**Programmatic Navigation API** +How the router exposes current route state and navigation methods. What objects/hooks are available to read the current route, its params, and matched route records. + +### 3. Compatibility Assessment + +At the end of the document, include a `## Compatibility` section with: + +- `compatible: true` or `compatible: false` +- If false, a `reason:` field explaining why + +A framework is **incompatible** if it lacks: +- A client-side route tree or route matching mechanism +- Dynamic segment parameters +- Navigation lifecycle events that can be hooked into +- Examples: Shopify Hydrogen (server-only loaders), Salesforce Lightning (proprietary component model) + +A framework is **compatible** even if: +- It uses file-based routing (as long as there's a runtime route representation) +- Some hooks fire later than ideal (trade-off is documented, not a blocker) + +## Output + +Write the result to `docs/integrations//01-router-concepts.md`. diff --git a/.claude/skills/router-generate/SKILL.md b/.claude/skills/router-generate/SKILL.md new file mode 100644 index 0000000000..4b93eef511 --- /dev/null +++ b/.claude/skills/router-generate/SKILL.md @@ -0,0 +1,94 @@ +--- +name: router:generate +description: "Stage 4: Generate the complete router integration package from design artifacts and reference implementations." +--- + +# Stage 4: Generate Package + +## Context + +You are Stage 4 of the router integration pipeline. Your job is to generate a complete, working Browser SDK router integration package based on the design decisions from Stage 3 and the patterns from reference implementations. + +## Input + +1. Read `docs/integrations//01-router-concepts.md` +2. Read `docs/integrations//02-sdk-mapping.md` +3. Read `docs/integrations//03-design-decisions.md` — this is your primary guide +4. Read reference implementations for code patterns (read the actual source files, not summaries): + - The reference implementation identified as "closest" in the design doc + - Plugin: e.g. `packages/rum-vue/src/domain/vuePlugin.ts` + - Router: e.g. `packages/rum-vue/src/domain/router/` + - Tests: corresponding `.spec.ts` files + - Test helper: e.g. `packages/rum-vue/src/test/initializeVuePlugin.ts` + - Package config: e.g. `packages/rum-vue/package.json`, `packages/rum-vue/tsconfig.json` + - Entry point: e.g. `packages/rum-vue/src/entries/main.ts` + +Find the `` directory by listing `docs/integrations/`. + +## Process + +### 1. Generate Each File + +Follow the file structure defined in `03-design-decisions.md`. For each file: + +1. Read the corresponding reference implementation file +2. Adapt it to the target framework using the mappings from `02-sdk-mapping.md` and decisions from `03-design-decisions.md` +3. Write the file using the Write tool + +**Code conventions** (from `AGENTS.md`): +- Use **camelCase** for all internal variables and object properties +- Conversion to snake_case happens at serialization boundary only +- Use TypeScript type narrowing over runtime assertions +- Follow existing patterns exactly — match import style, export style, comment style + +**Plugin file** (`Plugin.ts`): +- Follow the exact pattern from the reference: global state, subscriber arrays, `onRumInit`/`onRumStart` exports, `resetPlugin` for tests +- Plugin name must be the framework name in lowercase +- Configuration interface with `router?: boolean` +- `getConfigurationTelemetry` returning `{ router: !!configuration.router }` + +**Router integration files**: +- `types.ts`: minimal interface for route-related types, avoiding runtime framework imports where possible +- `startView.ts`: `computeViewName()` + `startRouterView()` calling `onRumInit` +- Integration point file: the framework-specific wrapper/provider/hook + +**Test files**: +- Follow Jasmine conventions: `describe`/`it` blocks +- Use `registerCleanupTask()` for cleanup, NOT `afterEach()` +- Use the test helper for plugin initialization +- Cover every test case listed in `03-design-decisions.md` + +**package.json**: +- Follow the reference `package.json` structure exactly +- Set version to match current monorepo version (read from a reference package) +- Set correct peer dependencies from `02-sdk-mapping.md` +- Include both ESM and CJS entry points + +**tsconfig.json**: +- Copy from nearest reference package, adjust paths + +**README.md**: +- Follow the reference README structure +- Include setup instructions matching the public API from `03-design-decisions.md` + +### 2. Write Generation Manifest + +After all files are generated, write `docs/integrations//04-generation-manifest.md`: + +```markdown +# Generation Manifest + +## Generated Files + +| File | Purpose | Modeled After | Deviations | +|------|---------|---------------|------------| +| `packages/rum-/src/domain/Plugin.ts` | Plugin + subscriber pattern | [`vuePlugin.ts`](packages/rum-vue/src/domain/vuePlugin.ts) | None | +| ... | ... | ... | ... | +``` + +For each file, link to the reference file it was modeled after. If there are deviations from the reference pattern, explain why. + +## Output + +- Generated package in `packages/rum-/` +- Manifest at `docs/integrations//04-generation-manifest.md` diff --git a/.claude/skills/router-pipeline/SKILL.md b/.claude/skills/router-pipeline/SKILL.md new file mode 100644 index 0000000000..a827ad413a --- /dev/null +++ b/.claude/skills/router-pipeline/SKILL.md @@ -0,0 +1,81 @@ +--- +name: router:pipeline +description: Fully automated pipeline that generates a draft Browser SDK router integration PR from framework public docs. Usage: /router:pipeline [...] +--- + +# Router Integration Pipeline + +You are an orchestrator that chains five stage skills to generate a complete Browser SDK router integration package and draft PR. + +## Input + +Parse the arguments: first argument is the framework name (lowercase), remaining arguments are documentation URLs. + +Example: `/router:pipeline svelte https://svelte.dev/docs/kit/routing` + +## Step 1: Initialize + +Create the artifact directory and write the input file: + +```bash +mkdir -p docs/integrations/ +``` + +Write `docs/integrations//00-pipeline-input.md`: + +```markdown +# Pipeline Input + +**Framework:** +**Documentation URLs:** +- +- +**Initiated:** +``` + +## Step 2: Invoke /router:fetch-docs + +Use the Skill tool to invoke `router:fetch-docs`. + +After completion, read `docs/integrations//01-router-concepts.md` and check the `compatible` field. + +If `compatible: false`, write `docs/integrations//EXIT.md` with: +- Stage: fetch-docs +- Reason: the incompatibility reason from 01-router-concepts.md +- Artifacts produced: 00-pipeline-input.md, 01-router-concepts.md + +Then stop and report the exit to the user. + +## Step 3: Invoke /router:analyze + +Use the Skill tool to invoke `router:analyze`. + +After completion, read `docs/integrations//02-sdk-mapping.md` and check for any concept marked `unmapped` with severity `critical`. + +If critical unmapped concepts exist, write `EXIT.md` with: +- Stage: analyze +- Reason: which critical concepts could not be mapped and why +- Artifacts produced: 00-pipeline-input.md, 01-router-concepts.md, 02-sdk-mapping.md + +Then stop and report the exit to the user. + +## Step 4: Invoke /router:design + +Use the Skill tool to invoke `router:design`. + +## Step 5: Invoke /router:generate + +Use the Skill tool to invoke `router:generate`. + +## Step 6: Invoke /router:pr + +Use the Skill tool to invoke `router:pr`. + +## Error Handling + +If any stage fails (fetch timeout, parse error, tool failure), write `EXIT.md` with: +- Stage: which stage failed +- Reason: error details +- Artifacts produced: list of files written before failure + +All artifacts written before the failure are preserved. Stop and report the failure to the user. diff --git a/.claude/skills/router-pr/SKILL.md b/.claude/skills/router-pr/SKILL.md new file mode 100644 index 0000000000..d2c3377628 --- /dev/null +++ b/.claude/skills/router-pr/SKILL.md @@ -0,0 +1,103 @@ +--- +name: router:pr +description: "Stage 5: Create a git branch, commit artifacts and generated code, and open a draft PR on GitHub." +--- + +# Stage 5: Create Draft PR + +## Context + +You are Stage 5 of the router integration pipeline. Your job is to create a branch, commit all generated artifacts and code, and open a draft PR on GitHub. + +## Input + +1. Read `docs/integrations//03-design-decisions.md` for PR body content +2. Read `docs/integrations//04-generation-manifest.md` for the list of generated files +3. Determine the current git user name from `git config user.name` (for branch naming) + +Find the `` directory by listing `docs/integrations/`. + +## Process + +### 1. Create Branch + +```bash +# Get the current user's branch prefix +USER=$(git config user.name | tr ' ' '.' | tr '[:upper:]' '[:lower:]') +BRANCH="${USER}/-router-integration" + +git checkout -b "$BRANCH" +``` + +### 2. Commit Artifacts + +First commit: design documentation only. + +```bash +git add docs/integrations// +git commit -m "📝 Add router integration design docs + +Generated by the router integration pipeline. +Artifacts: pipeline input, router concepts, SDK mapping, design decisions, generation manifest." +``` + +### 3. Commit Generated Package + +Second commit: the generated package. + +```bash +git add packages/rum-/ +git commit -m "✨ Add router integration package + +Auto-generated from framework documentation using the router integration pipeline. +See docs/integrations// for design artifacts and decision rationale." +``` + +### 4. Push and Create Draft PR + +```bash +git push -u origin "$BRANCH" +``` + +Then use `gh pr create` with this structure: + +```bash +gh pr create --draft --title "✨ Add router integration" --body "$(cat <<'PREOF' +## Summary + +Auto-generated router integration for using the router integration pipeline. + +- Router view tracking via `startView()` on route changes +- Error handler integration +- Unit tests for view name computation and navigation filtering + +## Design Artifacts + +Full design trail at `docs/integrations//`: +- `01-router-concepts.md` — extracted routing concepts from framework docs +- `02-sdk-mapping.md` — mapping to existing SDK patterns +- `03-design-decisions.md` — architecture and API decisions +- `04-generation-manifest.md` — list of generated files with lineage + +## Key Decisions + + + +## Test plan + +- [ ] Review design artifacts in `docs/integrations//` +- [ ] Review generated code against reference implementations +- [ ] Run `yarn typecheck` to verify type correctness +- [ ] Run `yarn test:unit --spec packages/rum-/` to verify tests pass +- [ ] Manual review of `computeViewName()` edge cases + +🤖 Generated with the router integration pipeline +PREOF +)" +``` + +Replace `` with the actual framework name. Replace the key decisions placeholder with actual content from `03-design-decisions.md`. + +## Output + +Report the PR URL to the user. From ece6de923e213bada98bf113383301748a4bf2ce Mon Sep 17 00:00:00 2001 From: Aymeric Mortemousque Date: Thu, 9 Apr 2026 11:23:24 +0200 Subject: [PATCH 04/12] =?UTF-8?q?=F0=9F=94=A7=20Remove=20error=20handler?= =?UTF-8?q?=20reference=20from=20router:pr=20skill?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/router-pr/SKILL.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.claude/skills/router-pr/SKILL.md b/.claude/skills/router-pr/SKILL.md index d2c3377628..21e39431e8 100644 --- a/.claude/skills/router-pr/SKILL.md +++ b/.claude/skills/router-pr/SKILL.md @@ -68,7 +68,6 @@ gh pr create --draft --title "✨ Add router integration" --body "$( Auto-generated router integration for using the router integration pipeline. - Router view tracking via `startView()` on route changes -- Error handler integration - Unit tests for view name computation and navigation filtering ## Design Artifacts From 2a5e64030e12778a4fa7d5a8960347fdbcbfcc7b Mon Sep 17 00:00:00 2001 From: Aymeric Mortemousque Date: Tue, 14 Apr 2026 15:14:13 +0200 Subject: [PATCH 05/12] First version --- .claude/skills/router-analyze/SKILL.md | 8 ++++- .claude/skills/router-design/SKILL.md | 7 +++- .claude/skills/router-fetch-docs/SKILL.md | 39 +++++++++++++++++++++-- .claude/skills/router-generate/SKILL.md | 17 +++++++--- .claude/skills/router-pipeline/SKILL.md | 28 +++++++++++++--- .claude/skills/router-pr/SKILL.md | 2 +- 6 files changed, 85 insertions(+), 16 deletions(-) diff --git a/.claude/skills/router-analyze/SKILL.md b/.claude/skills/router-analyze/SKILL.md index 9ccff031ac..9ddb0c6fe0 100644 --- a/.claude/skills/router-analyze/SKILL.md +++ b/.claude/skills/router-analyze/SKILL.md @@ -1,6 +1,6 @@ --- name: router:analyze -description: "Stage 2: Map framework router concepts to existing SDK patterns. Reads 01-router-concepts.md and reference implementations." +description: 'Stage 2: Map framework router concepts to existing SDK patterns. Reads 01-router-concepts.md and reference implementations.' --- # Stage 2: Analyze and Map to SDK Patterns @@ -24,6 +24,7 @@ Find the `` directory by listing `docs/integrations/`. ## Process For each concept in `01-router-concepts.md`, find the closest equivalent in the reference implementations. Every mapping MUST include inline links to both: + - The framework doc source (from 01-router-concepts.md links) - The specific file and line range in the reference implementation @@ -34,6 +35,7 @@ Which framework hook/event to subscribe to, and which reference implementation i **View Name Computation** How to build the `computeViewName()` function: + - How to access the matched route records after navigation - How dynamic segments appear in the route definition (and whether they need normalization) - How catch-all/wildcard routes should be substituted with actual path segments @@ -41,6 +43,7 @@ How to build the `computeViewName()` function: **Wrapping Strategy** How the integration hooks into the framework: + - Angular: [`ENVIRONMENT_INITIALIZER` provider](packages/rum-angular/src/domain/angularRouter/provideDatadogRouter.ts) - React: [wrapper around `createRouter`](packages/rum-react/src/domain/reactRouter/createRouter.ts) and [`useRoutes` hook](packages/rum-react/src/domain/reactRouter/useRoutes.ts) - Vue: [wrapper around `createRouter`](packages/rum-vue/src/domain/router/vueRouter.ts) @@ -52,6 +55,7 @@ Whether to define minimal local types (like Angular's [`RouteSnapshot`](packages **Plugin Configuration** How the plugin will be configured. All reference implementations use the same pattern: + - [`VuePluginConfiguration`](packages/rum-vue/src/domain/vuePlugin.ts) with `router?: boolean` - `onInit` sets `trackViewsManually = true` when `router: true` @@ -60,12 +64,14 @@ Which framework packages are required as peer dependencies, with version ranges. **Navigation Filtering** How to handle: + - Failed navigations (guards blocking, cancellations) - Duplicate navigations (same path) - Query-only changes - Initial navigation Reference the filtering logic in existing implementations: + - Vue: [lines 15-22 of vueRouter.ts](packages/rum-vue/src/domain/router/vueRouter.ts) - React: subscribe callback in [createRouter.ts](packages/rum-react/src/domain/reactRouter/createRouter.ts) diff --git a/.claude/skills/router-design/SKILL.md b/.claude/skills/router-design/SKILL.md index a382701c65..32677a164a 100644 --- a/.claude/skills/router-design/SKILL.md +++ b/.claude/skills/router-design/SKILL.md @@ -1,6 +1,6 @@ --- name: router:design -description: "Stage 3: Produce design decisions document from router concepts and SDK mapping. Reads 01 and 02 artifacts." +description: 'Stage 3: Produce design decisions document from router concepts and SDK mapping. Reads 01 and 02 artifacts.' --- # Stage 3: Design Decisions @@ -33,6 +33,7 @@ Produce a design document with these sections. Every decision MUST link back to **Public API** Exactly what the user imports and calls. Show the complete setup code example: + ```typescript // What the user writes in their app import { ... } from '@datadog/browser-rum-' @@ -40,6 +41,7 @@ import { ... } from '@datadog/browser-rum-' **File Structure** The exact file tree for `packages/rum-/` with one-line descriptions per file. Follow the convention from reference packages: + - `src/entries/main.ts` — public exports - `src/domain/Plugin.ts` — plugin + subscriber pattern - `src/domain/Router/` — router integration files @@ -51,6 +53,7 @@ Which hook to use, with the full trade-off analysis from Stage 1 and the mapping **View Name Algorithm** Pseudocode or step-by-step description of how `computeViewName()` will work for this framework. Cover: + - Normal routes - Dynamic segments - Nested routes @@ -58,6 +61,7 @@ Pseudocode or step-by-step description of how `computeViewName()` will work for - Edge cases specific to this framework IMPORTANT: The navigation hook choice directly affects what data is available to `computeViewName()`. Different hooks may expose different route objects, matched route arrays, or URL representations. If the framework has multiple candidate hooks (from the Navigation Hook Decision section), show how the view name computation differs for each option: + - What route data each hook provides (e.g. matched route records vs. raw URL vs. route config) - How that changes the `computeViewName()` implementation - Whether one hook gives better data for view name computation (e.g. access to parameterized route patterns vs. only resolved URLs) @@ -69,6 +73,7 @@ How failed, duplicate, query-only, and initial navigations are handled. **Test Strategy** List every test case to implement, grouped by file: + - `Plugin.spec.ts`: plugin structure, subscriber callbacks, telemetry, trackViewsManually - `startView.spec.ts`: all view name computation cases (static, dynamic, nested, catch-all, edge cases) - Router integration spec: navigation event handling, filtering, deduplication diff --git a/.claude/skills/router-fetch-docs/SKILL.md b/.claude/skills/router-fetch-docs/SKILL.md index bc51d4b93a..8a0cc1bec3 100644 --- a/.claude/skills/router-fetch-docs/SKILL.md +++ b/.claude/skills/router-fetch-docs/SKILL.md @@ -1,6 +1,6 @@ --- name: router:fetch-docs -description: "Stage 1: Fetch framework router documentation and extract structured routing concepts. Reads input from docs/integrations//00-pipeline-input.md." +description: 'Stage 1: Fetch framework router documentation and extract structured routing concepts. Reads input from docs/integrations//00-pipeline-input.md.' --- # Stage 1: Fetch Router Documentation @@ -29,7 +29,7 @@ Use the WebFetch tool for all fetches. If a URL fails, note it and continue with Analyze the fetched documentation and produce a structured summary covering these sections. Every factual claim MUST be inline-linked to the source URL and section where you found it. -Unsourced claims must be marked as *inferred: \*. +Unsourced claims must be marked as _inferred: \_. #### Required Sections @@ -44,17 +44,20 @@ What syntax the framework uses for catch-all or wildcard routes (e.g. `*`, `**`, **Navigation Lifecycle Hooks** List every navigation lifecycle event/hook the framework exposes, in the order they fire. For each, describe: + - When it fires relative to other hooks - What data is available at that point - Whether it can cancel/redirect navigation **Navigation Lifecycle Timing** This is the most critical section. Determine: + - Where in the lifecycle do **redirects** resolve? - Where do **data fetches** (loaders, resolvers) execute? - Where does **component rendering** begin? The SDK wants to start a view **as early as possible** but: + 1. **After redirects** — so the view name reflects the final destination 2. **Before data fetches** — so loader/resolver network requests are attributed to the correct view 3. **Before component rendering** — so rendering work is attributed to the correct view @@ -62,6 +65,7 @@ The SDK wants to start a view **as early as possible** but: Identify the ideal hook point. If no single hook satisfies all three constraints, document the trade-off and rank the available options from best to worst, explaining what is lost with each. Reference how existing integrations solve this: + - Angular uses [`GuardsCheckEnd`](packages/rum-angular/src/domain/angularRouter/provideDatadogRouter.ts) (after guards/redirects, before resolvers) - Vue uses [`afterEach`](packages/rum-vue/src/domain/router/vueRouter.ts) (after everything including render) - React uses [`subscribe`](packages/rum-react/src/domain/reactRouter/createRouter.ts) (after state change) @@ -72,7 +76,34 @@ How the framework matches URLs to routes: nested vs flat, layout routes, named o **Programmatic Navigation API** How the router exposes current route state and navigation methods. What objects/hooks are available to read the current route, its params, and matched route records. -### 3. Compatibility Assessment +### 3. Major Version History (Last 2 Years) + +Fetch the framework router's release/changelog information to identify major versions released within the last 2 years (since April 2024). + +**How to find versions:** + +Use GitHub Releases to identify major versions. Fetch `https://github.com///releases` and filter to major versions (semver X.0.0) released after April 2024. For each major version found, fetch its individual release page to get the full release notes and breaking changes. + +**For each major version, document:** + +- **Version number and release date** +- **Breaking changes** — list every breaking change from the release notes. Quote or link to the source. Do not filter, assess, or editorialize — just list them verbatim. + +**Output format:** + +```markdown +## Major Versions (Last 2 Years) + +### vX.0.0 (YYYY-MM-DD) + +**Breaking Changes:** +- Change description ([source](url)) +- ... +``` + +If no major versions were released in the last 2 years, state that explicitly. + +### 4. Compatibility Assessment At the end of the document, include a `## Compatibility` section with: @@ -80,12 +111,14 @@ At the end of the document, include a `## Compatibility` section with: - If false, a `reason:` field explaining why A framework is **incompatible** if it lacks: + - A client-side route tree or route matching mechanism - Dynamic segment parameters - Navigation lifecycle events that can be hooked into - Examples: Shopify Hydrogen (server-only loaders), Salesforce Lightning (proprietary component model) A framework is **compatible** even if: + - It uses file-based routing (as long as there's a runtime route representation) - Some hooks fire later than ideal (trade-off is documented, not a blocker) diff --git a/.claude/skills/router-generate/SKILL.md b/.claude/skills/router-generate/SKILL.md index 4b93eef511..a5bd4e47c8 100644 --- a/.claude/skills/router-generate/SKILL.md +++ b/.claude/skills/router-generate/SKILL.md @@ -1,6 +1,6 @@ --- name: router:generate -description: "Stage 4: Generate the complete router integration package from design artifacts and reference implementations." +description: 'Stage 4: Generate the complete router integration package from design artifacts and reference implementations.' --- # Stage 4: Generate Package @@ -36,38 +36,45 @@ Follow the file structure defined in `03-design-decisions.md`. For each file: 3. Write the file using the Write tool **Code conventions** (from `AGENTS.md`): + - Use **camelCase** for all internal variables and object properties - Conversion to snake_case happens at serialization boundary only - Use TypeScript type narrowing over runtime assertions - Follow existing patterns exactly — match import style, export style, comment style **Plugin file** (`Plugin.ts`): + - Follow the exact pattern from the reference: global state, subscriber arrays, `onRumInit`/`onRumStart` exports, `resetPlugin` for tests - Plugin name must be the framework name in lowercase - Configuration interface with `router?: boolean` - `getConfigurationTelemetry` returning `{ router: !!configuration.router }` **Router integration files**: + - `types.ts`: minimal interface for route-related types, avoiding runtime framework imports where possible - `startView.ts`: `computeViewName()` + `startRouterView()` calling `onRumInit` - Integration point file: the framework-specific wrapper/provider/hook **Test files**: + - Follow Jasmine conventions: `describe`/`it` blocks - Use `registerCleanupTask()` for cleanup, NOT `afterEach()` - Use the test helper for plugin initialization - Cover every test case listed in `03-design-decisions.md` **package.json**: + - Follow the reference `package.json` structure exactly - Set version to match current monorepo version (read from a reference package) - Set correct peer dependencies from `02-sdk-mapping.md` - Include both ESM and CJS entry points **tsconfig.json**: + - Copy from nearest reference package, adjust paths **README.md**: + - Follow the reference README structure - Include setup instructions matching the public API from `03-design-decisions.md` @@ -80,10 +87,10 @@ After all files are generated, write `docs/integrations//04-generatio ## Generated Files -| File | Purpose | Modeled After | Deviations | -|------|---------|---------------|------------| -| `packages/rum-/src/domain/Plugin.ts` | Plugin + subscriber pattern | [`vuePlugin.ts`](packages/rum-vue/src/domain/vuePlugin.ts) | None | -| ... | ... | ... | ... | +| File | Purpose | Modeled After | Deviations | +| -------------------------------------------- | --------------------------- | ---------------------------------------------------------- | ---------- | +| `packages/rum-/src/domain/Plugin.ts` | Plugin + subscriber pattern | [`vuePlugin.ts`](packages/rum-vue/src/domain/vuePlugin.ts) | None | +| ... | ... | ... | ... | ``` For each file, link to the reference file it was modeled after. If there are deviations from the reference pattern, explain why. diff --git a/.claude/skills/router-pipeline/SKILL.md b/.claude/skills/router-pipeline/SKILL.md index a827ad413a..83fa8ea955 100644 --- a/.claude/skills/router-pipeline/SKILL.md +++ b/.claude/skills/router-pipeline/SKILL.md @@ -1,6 +1,6 @@ --- name: router:pipeline -description: Fully automated pipeline that generates a draft Browser SDK router integration PR from framework public docs. Usage: /router:pipeline [...] +description: Fully automated pipeline that generates a draft Browser SDK router integration PR from an npm package URL. Usage: /router:pipeline --- # Router Integration Pipeline @@ -9,13 +9,25 @@ You are an orchestrator that chains five stage skills to generate a complete Bro ## Input -Parse the arguments: first argument is the framework name (lowercase), remaining arguments are documentation URLs. +The single argument is an npm package URL (e.g. `https://www.npmjs.com/package/@angular/router`). -Example: `/router:pipeline svelte https://svelte.dev/docs/kit/routing` +Example: `/router:pipeline https://www.npmjs.com/package/vue-router` -## Step 1: Initialize +## Step 1: Resolve Package Metadata & Initialize -Create the artifact directory and write the input file: +1. **Fetch npm package page** — Use WebFetch on the provided URL to extract: + - Package name (e.g. `vue-router`, `@angular/router`) + - Framework name — derive a lowercase identifier from the package name (e.g. `vue-router` → `vue`, `@angular/router` → `angular`, `@tanstack/react-router` → `tanstack-react-router`, `svelte` → `svelte`) + - Homepage / repository URL + - Keywords and description + +2. **Find router documentation URLs** — From the npm page metadata (homepage, repository links), locate the framework's official routing documentation: + - Check the homepage URL for docs links + - Check the GitHub repository README for documentation links + - Look for `/docs/`, `/guide/`, `/routing` paths on the framework's site + - Collect 1-3 relevant documentation URLs focused on routing + +3. **Create artifact directory and input file:** ```bash mkdir -p docs/integrations/ @@ -27,9 +39,12 @@ Write `docs/integrations//00-pipeline-input.md`: # Pipeline Input **Framework:** +**npm package:** **Documentation URLs:** + - - + **Initiated:** ``` @@ -40,6 +55,7 @@ Use the Skill tool to invoke `router:fetch-docs`. After completion, read `docs/integrations//01-router-concepts.md` and check the `compatible` field. If `compatible: false`, write `docs/integrations//EXIT.md` with: + - Stage: fetch-docs - Reason: the incompatibility reason from 01-router-concepts.md - Artifacts produced: 00-pipeline-input.md, 01-router-concepts.md @@ -53,6 +69,7 @@ Use the Skill tool to invoke `router:analyze`. After completion, read `docs/integrations//02-sdk-mapping.md` and check for any concept marked `unmapped` with severity `critical`. If critical unmapped concepts exist, write `EXIT.md` with: + - Stage: analyze - Reason: which critical concepts could not be mapped and why - Artifacts produced: 00-pipeline-input.md, 01-router-concepts.md, 02-sdk-mapping.md @@ -74,6 +91,7 @@ Use the Skill tool to invoke `router:pr`. ## Error Handling If any stage fails (fetch timeout, parse error, tool failure), write `EXIT.md` with: + - Stage: which stage failed - Reason: error details - Artifacts produced: list of files written before failure diff --git a/.claude/skills/router-pr/SKILL.md b/.claude/skills/router-pr/SKILL.md index 21e39431e8..3a7d89a3e7 100644 --- a/.claude/skills/router-pr/SKILL.md +++ b/.claude/skills/router-pr/SKILL.md @@ -1,6 +1,6 @@ --- name: router:pr -description: "Stage 5: Create a git branch, commit artifacts and generated code, and open a draft PR on GitHub." +description: 'Stage 5: Create a git branch, commit artifacts and generated code, and open a draft PR on GitHub.' --- # Stage 5: Create Draft PR From 8071b075cc09a1c946fb7529981ca3c7db02fff7 Mon Sep 17 00:00:00 2001 From: Aymeric Mortemousque Date: Tue, 14 Apr 2026 16:22:16 +0200 Subject: [PATCH 06/12] improve --- .claude/skills/router-analyze/SKILL.md | 2 +- .claude/skills/router-design/SKILL.md | 2 +- .claude/skills/router-fetch-docs/SKILL.md | 34 +++++++++++------------ .claude/skills/router-generate/SKILL.md | 2 +- .claude/skills/router-pipeline/SKILL.md | 26 ++++++++--------- .claude/skills/router-pr/SKILL.md | 2 +- 6 files changed, 33 insertions(+), 35 deletions(-) diff --git a/.claude/skills/router-analyze/SKILL.md b/.claude/skills/router-analyze/SKILL.md index 9ddb0c6fe0..395a3db6b7 100644 --- a/.claude/skills/router-analyze/SKILL.md +++ b/.claude/skills/router-analyze/SKILL.md @@ -1,5 +1,5 @@ --- -name: router:analyze +name: router-analyze description: 'Stage 2: Map framework router concepts to existing SDK patterns. Reads 01-router-concepts.md and reference implementations.' --- diff --git a/.claude/skills/router-design/SKILL.md b/.claude/skills/router-design/SKILL.md index 32677a164a..8b1e88511f 100644 --- a/.claude/skills/router-design/SKILL.md +++ b/.claude/skills/router-design/SKILL.md @@ -1,5 +1,5 @@ --- -name: router:design +name: router-design description: 'Stage 3: Produce design decisions document from router concepts and SDK mapping. Reads 01 and 02 artifacts.' --- diff --git a/.claude/skills/router-fetch-docs/SKILL.md b/.claude/skills/router-fetch-docs/SKILL.md index 8a0cc1bec3..7f86ed8bcc 100644 --- a/.claude/skills/router-fetch-docs/SKILL.md +++ b/.claude/skills/router-fetch-docs/SKILL.md @@ -1,5 +1,5 @@ --- -name: router:fetch-docs +name: router-fetch-docs description: 'Stage 1: Fetch framework router documentation and extract structured routing concepts. Reads input from docs/integrations//00-pipeline-input.md.' --- @@ -7,7 +7,9 @@ description: 'Stage 1: Fetch framework router documentation and extract structur ## Context -You are Stage 1 of the router integration pipeline. Your job is to fetch framework router documentation, extract structured routing concepts, and assess compatibility with the Browser SDK router integration model. +You are Stage 1 of the router integration pipeline. Your job is to fetch framework router documentation and extract structured routing concepts as factual reference material for later stages. + +Do NOT analyze which hooks the SDK should use, recommend approaches, or compare with existing SDK integrations. Only document what the framework provides. ## Input @@ -25,11 +27,18 @@ Before fetching the provided URLs directly, try to find LLM-friendly versions of Use the WebFetch tool for all fetches. If a URL fails, note it and continue with remaining URLs. If all URLs fail, write EXIT.md and stop. +**Prefer over-fetching to under-fetching.** Fetch every routing-related page you can find — API references, guides, tutorials. It is better to fetch a page and not need it than to miss information that a later stage requires. When in doubt, fetch it. + ### 2. Extract Router Concepts -Analyze the fetched documentation and produce a structured summary covering these sections. Every factual claim MUST be inline-linked to the source URL and section where you found it. +Analyze the fetched documentation and produce a structured summary covering these sections. -Unsourced claims must be marked as _inferred: \_. +#### Sourcing Rules + +- Every API name (class, interface, function, event, type) MUST be a clickable link to its API reference page (e.g. `[GuardsCheckEnd](https://angular.dev/api/router/GuardsCheckEnd)`). +- Do NOT use generic `([source](url))` links. Instead, make the API name itself the link. +- Every factual claim MUST be linked — either via an API link on the relevant name, or via a `([guide](url))` / `([commit](url))` link for non-API content. +- Unsourced claims must be marked as _inferred: \_. #### Required Sections @@ -50,25 +59,14 @@ List every navigation lifecycle event/hook the framework exposes, in the order t - Whether it can cancel/redirect navigation **Navigation Lifecycle Timing** -This is the most critical section. Determine: +Document the order of operations during a navigation. For each phase, state: - Where in the lifecycle do **redirects** resolve? - Where do **data fetches** (loaders, resolvers) execute? - Where does **component rendering** begin? +- Which hooks fire between each phase? -The SDK wants to start a view **as early as possible** but: - -1. **After redirects** — so the view name reflects the final destination -2. **Before data fetches** — so loader/resolver network requests are attributed to the correct view -3. **Before component rendering** — so rendering work is attributed to the correct view - -Identify the ideal hook point. If no single hook satisfies all three constraints, document the trade-off and rank the available options from best to worst, explaining what is lost with each. - -Reference how existing integrations solve this: - -- Angular uses [`GuardsCheckEnd`](packages/rum-angular/src/domain/angularRouter/provideDatadogRouter.ts) (after guards/redirects, before resolvers) -- Vue uses [`afterEach`](packages/rum-vue/src/domain/router/vueRouter.ts) (after everything including render) -- React uses [`subscribe`](packages/rum-react/src/domain/reactRouter/createRouter.ts) (after state change) +Include a timeline or ordered list showing the sequence: redirect resolution → guard evaluation → data fetching → component rendering, mapped to the specific hooks/events from the previous section. **Route Matching Model** How the framework matches URLs to routes: nested vs flat, layout routes, named outlets/slots, parallel routes. diff --git a/.claude/skills/router-generate/SKILL.md b/.claude/skills/router-generate/SKILL.md index a5bd4e47c8..7729a82ec0 100644 --- a/.claude/skills/router-generate/SKILL.md +++ b/.claude/skills/router-generate/SKILL.md @@ -1,5 +1,5 @@ --- -name: router:generate +name: router-generate description: 'Stage 4: Generate the complete router integration package from design artifacts and reference implementations.' --- diff --git a/.claude/skills/router-pipeline/SKILL.md b/.claude/skills/router-pipeline/SKILL.md index 83fa8ea955..7ff9b67d4a 100644 --- a/.claude/skills/router-pipeline/SKILL.md +++ b/.claude/skills/router-pipeline/SKILL.md @@ -1,6 +1,6 @@ --- -name: router:pipeline -description: Fully automated pipeline that generates a draft Browser SDK router integration PR from an npm package URL. Usage: /router:pipeline +name: router-pipeline +description: Fully automated pipeline that generates a draft Browser SDK router integration PR from an npm package URL. Usage: /router-pipeline --- # Router Integration Pipeline @@ -11,7 +11,7 @@ You are an orchestrator that chains five stage skills to generate a complete Bro The single argument is an npm package URL (e.g. `https://www.npmjs.com/package/@angular/router`). -Example: `/router:pipeline https://www.npmjs.com/package/vue-router` +Example: `/router-pipeline https://www.npmjs.com/package/vue-router` ## Step 1: Resolve Package Metadata & Initialize @@ -48,9 +48,9 @@ Write `docs/integrations//00-pipeline-input.md`: **Initiated:** ``` -## Step 2: Invoke /router:fetch-docs +## Step 2: Invoke /router-fetch-docs -Use the Skill tool to invoke `router:fetch-docs`. +Use the Skill tool to invoke `router-fetch-docs`. After completion, read `docs/integrations//01-router-concepts.md` and check the `compatible` field. @@ -62,9 +62,9 @@ If `compatible: false`, write `docs/integrations//EXIT.md` with: Then stop and report the exit to the user. -## Step 3: Invoke /router:analyze +## Step 3: Invoke /router-analyze -Use the Skill tool to invoke `router:analyze`. +Use the Skill tool to invoke `router-analyze`. After completion, read `docs/integrations//02-sdk-mapping.md` and check for any concept marked `unmapped` with severity `critical`. @@ -76,17 +76,17 @@ If critical unmapped concepts exist, write `EXIT.md` with: Then stop and report the exit to the user. -## Step 4: Invoke /router:design +## Step 4: Invoke /router-design -Use the Skill tool to invoke `router:design`. +Use the Skill tool to invoke `router-design`. -## Step 5: Invoke /router:generate +## Step 5: Invoke /router-generate -Use the Skill tool to invoke `router:generate`. +Use the Skill tool to invoke `router-generate`. -## Step 6: Invoke /router:pr +## Step 6: Invoke /router-pr -Use the Skill tool to invoke `router:pr`. +Use the Skill tool to invoke `router-pr`. ## Error Handling diff --git a/.claude/skills/router-pr/SKILL.md b/.claude/skills/router-pr/SKILL.md index 3a7d89a3e7..f8d5849320 100644 --- a/.claude/skills/router-pr/SKILL.md +++ b/.claude/skills/router-pr/SKILL.md @@ -1,5 +1,5 @@ --- -name: router:pr +name: router-pr description: 'Stage 5: Create a git branch, commit artifacts and generated code, and open a draft PR on GitHub.' --- From 93412acd995ae9a40a30306514886620905d1753 Mon Sep 17 00:00:00 2001 From: Aymeric Mortemousque Date: Tue, 14 Apr 2026 17:25:40 +0200 Subject: [PATCH 07/12] Merge step 2 and 3 --- .claude/skills/router-analyze/SKILL.md | 95 ------------------------- .claude/skills/router-design/SKILL.md | 95 ++++++++++++++++++++----- .claude/skills/router-generate/SKILL.md | 25 ++++--- .claude/skills/router-pipeline/SKILL.md | 20 +++--- .claude/skills/router-pr/SKILL.md | 21 +++--- 5 files changed, 107 insertions(+), 149 deletions(-) delete mode 100644 .claude/skills/router-analyze/SKILL.md diff --git a/.claude/skills/router-analyze/SKILL.md b/.claude/skills/router-analyze/SKILL.md deleted file mode 100644 index 395a3db6b7..0000000000 --- a/.claude/skills/router-analyze/SKILL.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -name: router-analyze -description: 'Stage 2: Map framework router concepts to existing SDK patterns. Reads 01-router-concepts.md and reference implementations.' ---- - -# Stage 2: Analyze and Map to SDK Patterns - -## Context - -You are Stage 2 of the router integration pipeline. Your job is to read the router concepts extracted in Stage 1 and map each one to the equivalent pattern in the existing Browser SDK framework integrations. - -## Input - -1. Read `docs/integrations//01-router-concepts.md` for the framework's router concepts -2. Read the reference implementations to understand the SDK contract: - - Plugin interface: `packages/rum-core/src/domain/plugins.ts` - - Public API: `packages/rum-core/src/boot/rumPublicApi.ts` - - Angular router: `packages/rum-angular/src/domain/angularRouter/` (all files) - - React router: `packages/rum-react/src/domain/reactRouter/` (all files) - - Vue router: `packages/rum-vue/src/domain/router/` (all files) - -Find the `` directory by listing `docs/integrations/`. - -## Process - -For each concept in `01-router-concepts.md`, find the closest equivalent in the reference implementations. Every mapping MUST include inline links to both: - -- The framework doc source (from 01-router-concepts.md links) -- The specific file and line range in the reference implementation - -### Required Mappings - -**Navigation Event Mapping** -Which framework hook/event to subscribe to, and which reference implementation it's most similar to. Justify the choice based on the lifecycle timing analysis from Stage 1 (after redirects, before data fetches, before render). If the chosen hook involves trade-offs, document them here with links to the Stage 1 analysis. - -**View Name Computation** -How to build the `computeViewName()` function: - -- How to access the matched route records after navigation -- How dynamic segments appear in the route definition (and whether they need normalization) -- How catch-all/wildcard routes should be substituted with actual path segments -- Link to the most similar existing `computeViewName` implementation and note differences - -**Wrapping Strategy** -How the integration hooks into the framework: - -- Angular: [`ENVIRONMENT_INITIALIZER` provider](packages/rum-angular/src/domain/angularRouter/provideDatadogRouter.ts) -- React: [wrapper around `createRouter`](packages/rum-react/src/domain/reactRouter/createRouter.ts) and [`useRoutes` hook](packages/rum-react/src/domain/reactRouter/useRoutes.ts) -- Vue: [wrapper around `createRouter`](packages/rum-vue/src/domain/router/vueRouter.ts) - -Determine which pattern fits this framework and why. - -**Type Strategy** -Whether to define minimal local types (like Angular's [`RouteSnapshot`](packages/rum-angular/src/domain/angularRouter/types.ts)) to avoid runtime framework imports, or import types directly from the framework package. - -**Plugin Configuration** -How the plugin will be configured. All reference implementations use the same pattern: - -- [`VuePluginConfiguration`](packages/rum-vue/src/domain/vuePlugin.ts) with `router?: boolean` -- `onInit` sets `trackViewsManually = true` when `router: true` - -**Peer Dependencies** -Which framework packages are required as peer dependencies, with version ranges. - -**Navigation Filtering** -How to handle: - -- Failed navigations (guards blocking, cancellations) -- Duplicate navigations (same path) -- Query-only changes -- Initial navigation - -Reference the filtering logic in existing implementations: - -- Vue: [lines 15-22 of vueRouter.ts](packages/rum-vue/src/domain/router/vueRouter.ts) -- React: subscribe callback in [createRouter.ts](packages/rum-react/src/domain/reactRouter/createRouter.ts) - -### Unmapped Concepts - -For any framework concept that has no SDK equivalent, create a section: - -```markdown -### Unmapped: - -**Severity:** critical | minor -**Reason:** -**Impact:** -``` - -- `critical`: the integration cannot work without this (e.g. no way to get route matches) -- `minor`: the integration works but this feature isn't supported (e.g. named outlets not tracked) - -## Output - -Write the result to `docs/integrations//02-sdk-mapping.md`. diff --git a/.claude/skills/router-design/SKILL.md b/.claude/skills/router-design/SKILL.md index 8b1e88511f..8994179e87 100644 --- a/.claude/skills/router-design/SKILL.md +++ b/.claude/skills/router-design/SKILL.md @@ -1,19 +1,24 @@ --- name: router-design -description: 'Stage 3: Produce design decisions document from router concepts and SDK mapping. Reads 01 and 02 artifacts.' +description: 'Stage 2: Analyze reference implementations and produce design decisions document from router concepts. Reads 01-router-concepts.md and reference code.' --- -# Stage 3: Design Decisions +# Stage 2: Design Decisions ## Context -You are Stage 3 of the router integration pipeline. Your job is to synthesize the router concepts (Stage 1) and SDK mapping (Stage 2) into a concrete design document that will guide code generation. +You are Stage 2 of the router integration pipeline. Your job is to read the router concepts extracted in Stage 1, analyze the existing reference implementations, and produce a concrete design document that will guide code generation. ## Input 1. Read `docs/integrations//01-router-concepts.md` -2. Read `docs/integrations//02-sdk-mapping.md` -3. Read reference implementations for structural patterns: +2. Read the reference implementations to understand the SDK contract: + - Plugin interface: `packages/rum-core/src/domain/plugins.ts` + - Public API: `packages/rum-core/src/boot/rumPublicApi.ts` + - Angular router: `packages/rum-angular/src/domain/angularRouter/` (all files) + - React router: `packages/rum-react/src/domain/reactRouter/` (all files) + - Vue router: `packages/rum-vue/src/domain/router/` (all files) +3. Read reference entry points and package configs: - Angular entry point: `packages/rum-angular/src/entries/main.ts` - Vue entry point: `packages/rum-vue/src/entries/main.ts` - React entry point: `packages/rum-react/src/entries/main.ts` @@ -24,7 +29,10 @@ Find the `` directory by listing `docs/integrations/`. ## Process -Produce a design document with these sections. Every decision MUST link back to the concept or mapping that informed it. +For each concept in `01-router-concepts.md`, find the closest equivalent in the reference implementations. Every mapping MUST include inline links to both: + +- The framework doc source (from 01-router-concepts.md links) +- The specific file and line range in the reference implementation ### Required Sections @@ -49,27 +57,59 @@ The exact file tree for `packages/rum-/` with one-line descriptions p - `package.json`, `tsconfig.json`, `README.md` **Navigation Hook Decision** -Which hook to use, with the full trade-off analysis from Stage 1 and the mapping from Stage 2. This is the most important architectural decision — make it explicit. +Which framework hook/event to subscribe to, and which reference implementation it's most similar to. Justify the choice based on the lifecycle timing analysis from Stage 1 (after redirects, before data fetches, before render). + +IMPORTANT: The navigation hook choice directly affects what data is available to `computeViewName()`. Different hooks may expose different route objects, matched route arrays, or URL representations. If the framework has multiple candidate hooks, show how the view name computation differs for each option: + +- What route data each hook provides (e.g. matched route records vs. raw URL vs. route config) +- How that changes the `computeViewName()` implementation +- Whether one hook gives better data for view name computation (e.g. access to parameterized route patterns vs. only resolved URLs) + +This analysis should reinforce or challenge the hook choice — if a hook that fires later provides significantly better route data, that trade-off must be documented. **View Name Algorithm** Pseudocode or step-by-step description of how `computeViewName()` will work for this framework. Cover: -- Normal routes -- Dynamic segments -- Nested routes -- Catch-all/wildcard routes +- How to access the matched route records after navigation +- How dynamic segments appear in the route definition (and whether they need normalization) +- How catch-all/wildcard routes should be substituted with actual path segments +- Normal routes, dynamic segments, nested routes, catch-all/wildcard routes - Edge cases specific to this framework +- Link to the most similar existing `computeViewName` implementation and note differences -IMPORTANT: The navigation hook choice directly affects what data is available to `computeViewName()`. Different hooks may expose different route objects, matched route arrays, or URL representations. If the framework has multiple candidate hooks (from the Navigation Hook Decision section), show how the view name computation differs for each option: +**Wrapping Strategy** +How the integration hooks into the framework. Reference implementations: -- What route data each hook provides (e.g. matched route records vs. raw URL vs. route config) -- How that changes the `computeViewName()` implementation -- Whether one hook gives better data for view name computation (e.g. access to parameterized route patterns vs. only resolved URLs) +- Angular: [`ENVIRONMENT_INITIALIZER` provider](packages/rum-angular/src/domain/angularRouter/provideDatadogRouter.ts) +- React: [wrapper around `createRouter`](packages/rum-react/src/domain/reactRouter/createRouter.ts) and [`useRoutes` hook](packages/rum-react/src/domain/reactRouter/useRoutes.ts) +- Vue: [wrapper around `createRouter`](packages/rum-vue/src/domain/router/vueRouter.ts) + +Determine which pattern fits this framework and why. + +**Type Strategy** +Whether to define minimal local types (like Angular's [`RouteSnapshot`](packages/rum-angular/src/domain/angularRouter/types.ts)) to avoid runtime framework imports, or import types directly from the framework package. + +**Plugin Configuration** +How the plugin will be configured. All reference implementations use the same pattern: -This analysis should reinforce or challenge the hook choice made above — if a hook that fires later provides significantly better route data, that trade-off must be documented. +- [`VuePluginConfiguration`](packages/rum-vue/src/domain/vuePlugin.ts) with `router?: boolean` +- `onInit` sets `trackViewsManually = true` when `router: true` + +**Peer Dependencies** +Which framework packages are required as peer dependencies, with version ranges. **Navigation Filtering** -How failed, duplicate, query-only, and initial navigations are handled. +How to handle: + +- Failed navigations (guards blocking, cancellations) +- Duplicate navigations (same path) +- Query-only changes +- Initial navigation + +Reference the filtering logic in existing implementations: + +- Vue: [lines 15-22 of vueRouter.ts](packages/rum-vue/src/domain/router/vueRouter.ts) +- React: subscribe callback in [createRouter.ts](packages/rum-react/src/domain/reactRouter/createRouter.ts) **Test Strategy** List every test case to implement, grouped by file: @@ -81,6 +121,25 @@ List every test case to implement, grouped by file: **Trade-offs and Alternatives** Document any decisions where multiple valid approaches existed. For each, state what was chosen, what was rejected, and why. +### Unmapped Concepts + +For any framework concept that has no SDK equivalent, create a section: + +```markdown +### Unmapped: + +**Severity:** critical | minor +**Reason:** +**Impact:** +``` + +- `critical`: the integration cannot work without this (e.g. no way to get route matches) — **stop the pipeline** +- `minor`: the integration works but this feature isn't supported (e.g. named outlets not tracked) + +## Exit Criteria + +If any unmapped concept has severity `critical`, stop the pipeline. Write an exit note at the top of the output explaining which concepts could not be mapped and why. + ## Output -Write the result to `docs/integrations//03-design-decisions.md`. +Write the result to `docs/integrations//02-design-decisions.md`. diff --git a/.claude/skills/router-generate/SKILL.md b/.claude/skills/router-generate/SKILL.md index 7729a82ec0..14396b0666 100644 --- a/.claude/skills/router-generate/SKILL.md +++ b/.claude/skills/router-generate/SKILL.md @@ -1,20 +1,19 @@ --- name: router-generate -description: 'Stage 4: Generate the complete router integration package from design artifacts and reference implementations.' +description: 'Stage 3: Generate the complete router integration package from design artifacts and reference implementations.' --- -# Stage 4: Generate Package +# Stage 3: Generate Package ## Context -You are Stage 4 of the router integration pipeline. Your job is to generate a complete, working Browser SDK router integration package based on the design decisions from Stage 3 and the patterns from reference implementations. +You are Stage 3 of the router integration pipeline. Your job is to generate a complete, working Browser SDK router integration package based on the design decisions from Stage 2 and the patterns from reference implementations. ## Input 1. Read `docs/integrations//01-router-concepts.md` -2. Read `docs/integrations//02-sdk-mapping.md` -3. Read `docs/integrations//03-design-decisions.md` — this is your primary guide -4. Read reference implementations for code patterns (read the actual source files, not summaries): +2. Read `docs/integrations//02-design-decisions.md` — this is your primary guide +3. Read reference implementations for code patterns (read the actual source files, not summaries): - The reference implementation identified as "closest" in the design doc - Plugin: e.g. `packages/rum-vue/src/domain/vuePlugin.ts` - Router: e.g. `packages/rum-vue/src/domain/router/` @@ -29,10 +28,10 @@ Find the `` directory by listing `docs/integrations/`. ### 1. Generate Each File -Follow the file structure defined in `03-design-decisions.md`. For each file: +Follow the file structure defined in `02-design-decisions.md`. For each file: 1. Read the corresponding reference implementation file -2. Adapt it to the target framework using the mappings from `02-sdk-mapping.md` and decisions from `03-design-decisions.md` +2. Adapt it to the target framework using the decisions from `02-design-decisions.md` 3. Write the file using the Write tool **Code conventions** (from `AGENTS.md`): @@ -60,13 +59,13 @@ Follow the file structure defined in `03-design-decisions.md`. For each file: - Follow Jasmine conventions: `describe`/`it` blocks - Use `registerCleanupTask()` for cleanup, NOT `afterEach()` - Use the test helper for plugin initialization -- Cover every test case listed in `03-design-decisions.md` +- Cover every test case listed in `02-design-decisions.md` **package.json**: - Follow the reference `package.json` structure exactly - Set version to match current monorepo version (read from a reference package) -- Set correct peer dependencies from `02-sdk-mapping.md` +- Set correct peer dependencies from `02-design-decisions.md` - Include both ESM and CJS entry points **tsconfig.json**: @@ -76,11 +75,11 @@ Follow the file structure defined in `03-design-decisions.md`. For each file: **README.md**: - Follow the reference README structure -- Include setup instructions matching the public API from `03-design-decisions.md` +- Include setup instructions matching the public API from `02-design-decisions.md` ### 2. Write Generation Manifest -After all files are generated, write `docs/integrations//04-generation-manifest.md`: +After all files are generated, write `docs/integrations//03-generation-manifest.md`: ```markdown # Generation Manifest @@ -98,4 +97,4 @@ For each file, link to the reference file it was modeled after. If there are dev ## Output - Generated package in `packages/rum-/` -- Manifest at `docs/integrations//04-generation-manifest.md` +- Manifest at `docs/integrations//03-generation-manifest.md` diff --git a/.claude/skills/router-pipeline/SKILL.md b/.claude/skills/router-pipeline/SKILL.md index 7ff9b67d4a..0556c58936 100644 --- a/.claude/skills/router-pipeline/SKILL.md +++ b/.claude/skills/router-pipeline/SKILL.md @@ -5,7 +5,7 @@ description: Fully automated pipeline that generates a draft Browser SDK router # Router Integration Pipeline -You are an orchestrator that chains five stage skills to generate a complete Browser SDK router integration package and draft PR. +You are an orchestrator that chains four stage skills to generate a complete Browser SDK router integration package and draft PR. ## Input @@ -62,29 +62,25 @@ If `compatible: false`, write `docs/integrations//EXIT.md` with: Then stop and report the exit to the user. -## Step 3: Invoke /router-analyze +## Step 3: Invoke /router-design -Use the Skill tool to invoke `router-analyze`. +Use the Skill tool to invoke `router-design`. -After completion, read `docs/integrations//02-sdk-mapping.md` and check for any concept marked `unmapped` with severity `critical`. +After completion, read `docs/integrations//02-design-decisions.md` and check for any concept marked `unmapped` with severity `critical`. If critical unmapped concepts exist, write `EXIT.md` with: -- Stage: analyze +- Stage: design - Reason: which critical concepts could not be mapped and why -- Artifacts produced: 00-pipeline-input.md, 01-router-concepts.md, 02-sdk-mapping.md +- Artifacts produced: 00-pipeline-input.md, 01-router-concepts.md, 02-design-decisions.md Then stop and report the exit to the user. -## Step 4: Invoke /router-design - -Use the Skill tool to invoke `router-design`. - -## Step 5: Invoke /router-generate +## Step 4: Invoke /router-generate Use the Skill tool to invoke `router-generate`. -## Step 6: Invoke /router-pr +## Step 5: Invoke /router-pr Use the Skill tool to invoke `router-pr`. diff --git a/.claude/skills/router-pr/SKILL.md b/.claude/skills/router-pr/SKILL.md index f8d5849320..73040896a1 100644 --- a/.claude/skills/router-pr/SKILL.md +++ b/.claude/skills/router-pr/SKILL.md @@ -1,18 +1,18 @@ --- name: router-pr -description: 'Stage 5: Create a git branch, commit artifacts and generated code, and open a draft PR on GitHub.' +description: 'Stage 4: Create a git branch, commit artifacts and generated code, and open a draft PR on GitHub.' --- -# Stage 5: Create Draft PR +# Stage 4: Create Draft PR ## Context -You are Stage 5 of the router integration pipeline. Your job is to create a branch, commit all generated artifacts and code, and open a draft PR on GitHub. +You are Stage 4 of the router integration pipeline. Your job is to create a branch, commit all generated artifacts and code, and open a draft PR on GitHub. ## Input -1. Read `docs/integrations//03-design-decisions.md` for PR body content -2. Read `docs/integrations//04-generation-manifest.md` for the list of generated files +1. Read `docs/integrations//02-design-decisions.md` for PR body content +2. Read `docs/integrations//03-generation-manifest.md` for the list of generated files 3. Determine the current git user name from `git config user.name` (for branch naming) Find the `` directory by listing `docs/integrations/`. @@ -38,7 +38,7 @@ git add docs/integrations// git commit -m "📝 Add router integration design docs Generated by the router integration pipeline. -Artifacts: pipeline input, router concepts, SDK mapping, design decisions, generation manifest." +Artifacts: pipeline input, router concepts, design decisions, generation manifest." ``` ### 3. Commit Generated Package @@ -74,13 +74,12 @@ Auto-generated router integration for using the router integration p Full design trail at `docs/integrations//`: - `01-router-concepts.md` — extracted routing concepts from framework docs -- `02-sdk-mapping.md` — mapping to existing SDK patterns -- `03-design-decisions.md` — architecture and API decisions -- `04-generation-manifest.md` — list of generated files with lineage +- `02-design-decisions.md` — SDK mapping, architecture and API decisions +- `03-generation-manifest.md` — list of generated files with lineage ## Key Decisions - + ## Test plan @@ -95,7 +94,7 @@ PREOF )" ``` -Replace `` with the actual framework name. Replace the key decisions placeholder with actual content from `03-design-decisions.md`. +Replace `` with the actual framework name. Replace the key decisions placeholder with actual content from `02-design-decisions.md`. ## Output From 8a928aa9923e9651b3b732b321f4408a89f419f3 Mon Sep 17 00:00:00 2001 From: Aymeric Mortemousque Date: Wed, 15 Apr 2026 11:01:09 +0200 Subject: [PATCH 08/12] Remove Angular --- eslint-local-rules/disallowSideEffects.js | 3 - eslint.config.mjs | 1 - packages/rum-angular/LICENSE | 201 -- packages/rum-angular/README.md | 163 -- packages/rum-angular/package.json | 46 - .../src/domain/angularPlugin.spec.ts | 98 - .../rum-angular/src/domain/angularPlugin.ts | 93 - .../angularRouter/provideDatadogRouter.ts | 64 - .../angularRouter/startAngularView.spec.ts | 101 - .../domain/angularRouter/startAngularView.ts | 43 - .../src/domain/angularRouter/types.ts | 10 - .../src/domain/error/addAngularError.spec.ts | 63 - .../src/domain/error/addAngularError.ts | 39 - .../rum-angular/src/domain/error/index.ts | 2 - .../error/provideDatadogErrorHandler.spec.ts | 32 - .../error/provideDatadogErrorHandler.ts | 28 - packages/rum-angular/src/entries/main.ts | 4 - .../test/initializeAngularPlugin.ts | 23 - packages/rum-angular/tsconfig.json | 9 - scripts/build/build-test-apps.ts | 1 - test/apps/angular-app/.gitignore | 3 - test/apps/angular-app/main.ts | 100 - test/apps/angular-app/package.json | 47 - test/apps/angular-app/tsconfig.json | 20 - test/apps/angular-app/webpack.config.js | 43 - test/apps/angular-app/yarn.lock | 2491 ----------------- test/e2e/lib/framework/sdkBuilds.ts | 1 - test/e2e/lib/framework/serverApps/mock.ts | 2 +- test/e2e/scenario/angularPlugin.scenario.ts | 108 - test/unit/globalThisPolyfill.js | 6 - test/unit/karma.base.conf.js | 5 +- yarn.lock | 76 +- 32 files changed, 3 insertions(+), 3923 deletions(-) delete mode 100644 packages/rum-angular/LICENSE delete mode 100644 packages/rum-angular/README.md delete mode 100644 packages/rum-angular/package.json delete mode 100644 packages/rum-angular/src/domain/angularPlugin.spec.ts delete mode 100644 packages/rum-angular/src/domain/angularPlugin.ts delete mode 100644 packages/rum-angular/src/domain/angularRouter/provideDatadogRouter.ts delete mode 100644 packages/rum-angular/src/domain/angularRouter/startAngularView.spec.ts delete mode 100644 packages/rum-angular/src/domain/angularRouter/startAngularView.ts delete mode 100644 packages/rum-angular/src/domain/angularRouter/types.ts delete mode 100644 packages/rum-angular/src/domain/error/addAngularError.spec.ts delete mode 100644 packages/rum-angular/src/domain/error/addAngularError.ts delete mode 100644 packages/rum-angular/src/domain/error/index.ts delete mode 100644 packages/rum-angular/src/domain/error/provideDatadogErrorHandler.spec.ts delete mode 100644 packages/rum-angular/src/domain/error/provideDatadogErrorHandler.ts delete mode 100644 packages/rum-angular/src/entries/main.ts delete mode 100644 packages/rum-angular/test/initializeAngularPlugin.ts delete mode 100644 packages/rum-angular/tsconfig.json delete mode 100644 test/apps/angular-app/.gitignore delete mode 100644 test/apps/angular-app/main.ts delete mode 100644 test/apps/angular-app/package.json delete mode 100644 test/apps/angular-app/tsconfig.json delete mode 100644 test/apps/angular-app/webpack.config.js delete mode 100644 test/apps/angular-app/yarn.lock delete mode 100644 test/e2e/scenario/angularPlugin.scenario.ts delete mode 100644 test/unit/globalThisPolyfill.js diff --git a/eslint-local-rules/disallowSideEffects.js b/eslint-local-rules/disallowSideEffects.js index 890c5852df..2ade75eb10 100644 --- a/eslint-local-rules/disallowSideEffects.js +++ b/eslint-local-rules/disallowSideEffects.js @@ -40,9 +40,6 @@ const packagesWithoutSideEffect = new Set([ 'react-router-dom', 'vue', 'vue-router', - '@angular/core', - '@angular/router', - 'rxjs', ]) /** diff --git a/eslint.config.mjs b/eslint.config.mjs index 595675bdc7..57551de418 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -32,7 +32,6 @@ export default tseslint.config( 'test/apps/react-shopist-like', 'test/apps/microfrontend', 'test/apps/nextjs', - 'test/apps/angular-app', 'test/apps/vue-router-app', 'sandbox', 'coverage', diff --git a/packages/rum-angular/LICENSE b/packages/rum-angular/LICENSE deleted file mode 100644 index e6d7fbc979..0000000000 --- a/packages/rum-angular/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2019-Present Datadog, Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/packages/rum-angular/README.md b/packages/rum-angular/README.md deleted file mode 100644 index d6f53dd529..0000000000 --- a/packages/rum-angular/README.md +++ /dev/null @@ -1,163 +0,0 @@ -# RUM Browser Monitoring - Angular integration - -> **Note**: This integration is in beta. Features and configuration may change. - -## Overview - -With the Datadog RUM Angular integration, resolve performance issues quickly in Angular applications by: - -- Debugging the root cause of performance bottlenecks, such as a slow server response time, render-blocking resource, or an error inside a component -- Automatically correlating web performance data with user journeys, HTTP calls, and logs -- Alerting your engineering teams when crucial web performance metrics (such as Core Web Vitals) fall below a threshold that results in a poor user experience - -Monitor your Angular applications from end-to-end by: - -- Tracking and visualizing user journeys across your entire stack -- Debugging the root cause of slow load times, which may be an issue with your Angular code, network performance, or underlying infrastructure -- Analyzing and contextualizing every user session with attributes such as user ID, email, name, and more -- Unifying full-stack monitoring in one platform for frontend and backend development teams - -## Setup - -Start by setting up [Datadog RUM][1] in your Angular application. If you're creating a new RUM application in the Datadog App, select Angular as the application type. If you already have an existing RUM application, you can update its type to Angular instead. Once configured, the Datadog App will provide instructions for integrating the [RUM-Angular plugin][2] with the Browser SDK. If Angular is not available as an option, follow the steps below to integrate the plugin manually. - -## Usage - -### 1. Initialize the Datadog RUM SDK with the Angular plugin - -```typescript -import { datadogRum } from '@datadog/browser-rum' -import { angularPlugin } from '@datadog/browser-rum-angular' - -datadogRum.init({ - applicationId: '', - clientToken: '', - site: 'datadoghq.com', - plugins: [angularPlugin()], -}) -``` - -## Error Tracking - -To track errors that occur inside Angular components, you can either use the built-in provider or report errors manually from your own error handler. - -### `provideDatadogErrorHandler` usage - -`provideDatadogErrorHandler()` replaces Angular's default `ErrorHandler` with one that reports errors to Datadog RUM. It preserves the default `console.error` behavior. - -**Standalone setup:** - -```typescript -import { bootstrapApplication } from '@angular/platform-browser' -import { angularPlugin, provideDatadogErrorHandler } from '@datadog/browser-rum-angular' -import { datadogRum } from '@datadog/browser-rum' - -datadogRum.init({ - ... - plugins: [angularPlugin()], -}) - -bootstrapApplication(AppComponent, { - providers: [provideDatadogErrorHandler()], -}) -``` - -**NgModule setup:** - -```typescript -import { angularPlugin, provideDatadogErrorHandler } from '@datadog/browser-rum-angular' -import { datadogRum } from '@datadog/browser-rum' - -datadogRum.init({ - ... - plugins: [angularPlugin()], -}) - -@NgModule({ - providers: [provideDatadogErrorHandler()], -}) -export class AppModule {} -``` - -### Reporting Angular errors from your own `ErrorHandler` - -If you already have a custom `ErrorHandler`, use `addAngularError` to report errors to Datadog without replacing your handler: - -```typescript -import { ErrorHandler } from '@angular/core' -import { addAngularError } from '@datadog/browser-rum-angular' - -class MyCustomErrorHandler implements ErrorHandler { - handleError(error: unknown): void { - addAngularError(error) - // ... custom logic (show toast, log to service, etc.) - } -} -``` - -## Angular Router Integration - -To track route changes with Angular's built-in router, initialize the `angularPlugin` with the `router: true` option and add `provideDatadogRouter()` to your providers. - -**Standalone setup:** - -```typescript -import { bootstrapApplication } from '@angular/platform-browser' -import { provideRouter } from '@angular/router' -import { angularPlugin, provideDatadogRouter } from '@datadog/browser-rum-angular' -import { datadogRum } from '@datadog/browser-rum' - -datadogRum.init({ - ... - plugins: [angularPlugin({ router: true })], -}) - -bootstrapApplication(AppComponent, { - providers: [provideRouter(routes), provideDatadogRouter()], -}) -``` - -**NgModule setup:** - -```typescript -import { angularPlugin, provideDatadogRouter } from '@datadog/browser-rum-angular' -import { datadogRum } from '@datadog/browser-rum' - -datadogRum.init({ - ... - plugins: [angularPlugin({ router: true })], -}) - -@NgModule({ - imports: [RouterModule.forRoot(routes)], - providers: [provideDatadogRouter()], -}) -export class AppModule {} -``` - -When enabled, the integration uses route patterns as view names instead of resolved URLs. For example, navigating to `/article/2` generates a view named `/article/:articleId` instead. - -## Go Further with Datadog Angular Integration - -### Traces - -Connect your RUM and trace data to get a complete view of your application's performance. See [Connect RUM and Traces][3]. - -### Logs - -To start forwarding your Angular application's logs to Datadog, see [JavaScript Log Collection][4]. - -### Metrics - -To generate custom metrics from your RUM application, see [Generate Metrics][5]. - -## Troubleshooting - -Need help? Contact [Datadog Support][6]. - -[1]: https://docs.datadoghq.com/real_user_monitoring/browser/setup/client -[2]: https://www.npmjs.com/package/@datadog/browser-rum-angular -[3]: https://docs.datadoghq.com/real_user_monitoring/platform/connect_rum_and_traces/?tab=browserrum -[4]: https://docs.datadoghq.com/logs/log_collection/javascript/ -[5]: https://docs.datadoghq.com/real_user_monitoring/generate_metrics -[6]: https://docs.datadoghq.com/help/ diff --git a/packages/rum-angular/package.json b/packages/rum-angular/package.json deleted file mode 100644 index c9ccd840ee..0000000000 --- a/packages/rum-angular/package.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "@datadog/browser-rum-angular", - "version": "6.32.0", - "license": "Apache-2.0", - "main": "cjs/entries/main.js", - "module": "esm/entries/main.js", - "types": "cjs/entries/main.d.ts", - "files": [ - "cjs", - "esm", - "src", - "!src/**/*.spec.ts", - "!src/**/*.specHelper.ts" - ], - "scripts": { - "build": "node ../../scripts/build/build-package.ts --modules", - "prepack": "npm run build" - }, - "dependencies": { - "@datadog/browser-core": "6.32.0", - "@datadog/browser-rum-core": "6.32.0" - }, - "peerDependencies": { - "@angular/core": ">=15 <=21", - "@angular/router": ">=15 <=21", - "rxjs": ">=7" - }, - "repository": { - "type": "git", - "url": "https://github.com/DataDog/browser-sdk.git", - "directory": "packages/rum-angular" - }, - "volta": { - "extends": "../../package.json" - }, - "publishConfig": { - "access": "public" - }, - "devDependencies": { - "@angular/common": "19.2.5", - "@angular/compiler": "19.2.5", - "@angular/core": "19.2.5", - "@angular/router": "19.2.5", - "rxjs": "7.8.2" - } -} diff --git a/packages/rum-angular/src/domain/angularPlugin.spec.ts b/packages/rum-angular/src/domain/angularPlugin.spec.ts deleted file mode 100644 index 2f948bffa2..0000000000 --- a/packages/rum-angular/src/domain/angularPlugin.spec.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { RumInitConfiguration, RumPublicApi } from '@datadog/browser-rum-core' -import { registerCleanupTask } from '../../../core/test' -import { angularPlugin, onRumInit, onRumStart, resetAngularPlugin } from './angularPlugin' - -const PUBLIC_API = {} as RumPublicApi -const INIT_CONFIGURATION = {} as RumInitConfiguration - -describe('angularPlugin', () => { - beforeEach(() => { - registerCleanupTask(() => { - resetAngularPlugin() - }) - }) - - it('returns a plugin object', () => { - const plugin = angularPlugin() - expect(plugin).toEqual( - jasmine.objectContaining({ - name: 'angular', - onInit: jasmine.any(Function), - onRumStart: jasmine.any(Function), - }) - ) - }) - - it('calls callbacks registered with onRumInit during onInit', () => { - const callbackSpy = jasmine.createSpy() - const pluginConfiguration = {} - onRumInit(callbackSpy) - - expect(callbackSpy).not.toHaveBeenCalled() - - angularPlugin(pluginConfiguration).onInit!({ - publicApi: PUBLIC_API, - initConfiguration: INIT_CONFIGURATION, - }) - - expect(callbackSpy).toHaveBeenCalledTimes(1) - expect(callbackSpy.calls.mostRecent().args[0]).toBe(pluginConfiguration) - expect(callbackSpy.calls.mostRecent().args[1]).toBe(PUBLIC_API) - }) - - it('calls callbacks immediately if onInit was already invoked', () => { - const callbackSpy = jasmine.createSpy() - const pluginConfiguration = {} - angularPlugin(pluginConfiguration).onInit!({ - publicApi: PUBLIC_API, - initConfiguration: INIT_CONFIGURATION, - }) - - onRumInit(callbackSpy) - - expect(callbackSpy).toHaveBeenCalledTimes(1) - expect(callbackSpy.calls.mostRecent().args[0]).toBe(pluginConfiguration) - expect(callbackSpy.calls.mostRecent().args[1]).toBe(PUBLIC_API) - }) - - it('enforce manual view tracking when router is enabled', () => { - const initConfiguration = { ...INIT_CONFIGURATION } - angularPlugin({ router: true }).onInit!({ publicApi: PUBLIC_API, initConfiguration }) - - expect(initConfiguration.trackViewsManually).toBe(true) - }) - - it('does not enforce manual view tracking when router is disabled', () => { - const initConfiguration = { ...INIT_CONFIGURATION } - angularPlugin({ router: false }).onInit!({ publicApi: PUBLIC_API, initConfiguration }) - - expect(initConfiguration.trackViewsManually).toBeUndefined() - }) - - it('returns the configuration telemetry', () => { - const pluginConfiguration = { router: true } - const plugin = angularPlugin(pluginConfiguration) - - expect(plugin.getConfigurationTelemetry!()).toEqual({ router: true }) - }) - - it('calls onRumStart subscribers during onRumStart', () => { - const callbackSpy = jasmine.createSpy() - const addErrorSpy = jasmine.createSpy() - onRumStart(callbackSpy) - - angularPlugin().onRumStart!({ addError: addErrorSpy }) - - expect(callbackSpy).toHaveBeenCalledWith(addErrorSpy) - }) - - it('calls onRumStart subscribers immediately if already started', () => { - const addErrorSpy = jasmine.createSpy() - angularPlugin().onRumStart!({ addError: addErrorSpy }) - - const callbackSpy = jasmine.createSpy() - onRumStart(callbackSpy) - - expect(callbackSpy).toHaveBeenCalledWith(addErrorSpy) - }) -}) diff --git a/packages/rum-angular/src/domain/angularPlugin.ts b/packages/rum-angular/src/domain/angularPlugin.ts deleted file mode 100644 index 2a8163f71f..0000000000 --- a/packages/rum-angular/src/domain/angularPlugin.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { RumPlugin, RumPublicApi, StartRumResult } from '@datadog/browser-rum-core' - -type InitSubscriber = (configuration: AngularPluginConfiguration, rumPublicApi: RumPublicApi) => void -type StartSubscriber = (addError: StartRumResult['addError']) => void - -let globalPublicApi: RumPublicApi | undefined -let globalConfiguration: AngularPluginConfiguration | undefined -let globalAddError: StartRumResult['addError'] | undefined - -const onRumInitSubscribers: InitSubscriber[] = [] -const onRumStartSubscribers: StartSubscriber[] = [] - -/** - * Angular plugin configuration. - * - * @category Main - */ -export interface AngularPluginConfiguration { - /** - * Enable Angular Router integration. Make sure to use `provideDatadogRouter()` in your - * application providers. - */ - router?: boolean -} - -/** - * Angular plugin constructor. - * - * @category Main - * @example - * ```ts - * import { datadogRum } from '@datadog/browser-rum' - * import { angularPlugin } from '@datadog/browser-rum-angular' - * - * datadogRum.init({ - * applicationId: '', - * clientToken: '', - * site: '', - * plugins: [angularPlugin({ router: true })], - * // ... - * }) - * ``` - */ -export function angularPlugin(configuration: AngularPluginConfiguration = {}): RumPlugin { - return { - name: 'angular', - onInit({ publicApi, initConfiguration }) { - globalPublicApi = publicApi - globalConfiguration = configuration - for (const subscriber of onRumInitSubscribers) { - subscriber(globalConfiguration, globalPublicApi) - } - if (configuration.router) { - initConfiguration.trackViewsManually = true - } - }, - onRumStart({ addError }) { - globalAddError = addError - if (addError) { - for (const subscriber of onRumStartSubscribers) { - subscriber(addError) - } - } - }, - getConfigurationTelemetry() { - return { router: !!configuration.router } - }, - } satisfies RumPlugin -} - -export function onRumInit(callback: InitSubscriber) { - if (globalConfiguration && globalPublicApi) { - callback(globalConfiguration, globalPublicApi) - } else { - onRumInitSubscribers.push(callback) - } -} - -export function onRumStart(callback: StartSubscriber) { - if (globalAddError) { - callback(globalAddError) - } else { - onRumStartSubscribers.push(callback) - } -} - -export function resetAngularPlugin() { - globalPublicApi = undefined - globalConfiguration = undefined - globalAddError = undefined - onRumInitSubscribers.length = 0 - onRumStartSubscribers.length = 0 -} diff --git a/packages/rum-angular/src/domain/angularRouter/provideDatadogRouter.ts b/packages/rum-angular/src/domain/angularRouter/provideDatadogRouter.ts deleted file mode 100644 index 034c2b2755..0000000000 --- a/packages/rum-angular/src/domain/angularRouter/provideDatadogRouter.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { EnvironmentProviders } from '@angular/core' -import { ENVIRONMENT_INITIALIZER, inject, makeEnvironmentProviders } from '@angular/core' -import { GuardsCheckEnd, Router } from '@angular/router' -import { filter } from 'rxjs' -import { startAngularView } from './startAngularView' - -/** - * Angular provider that subscribes to Router events and starts a new RUM view - * on each GuardsCheckEnd, using the matched route template as the view name. - * - * GuardsCheckEnd fires after guards pass but before resolvers run, so data - * fetches from resolvers are correctly attributed to the new view. - * - * @category Main - * @example - * ```ts - * import { bootstrapApplication } from '@angular/platform-browser' - * import { provideRouter } from '@angular/router' - * import { provideDatadogRouter } from '@datadog/browser-rum-angular' - * - * bootstrapApplication(AppComponent, { - * providers: [ - * provideRouter(routes), - * provideDatadogRouter(), - * ], - * }) - * ``` - */ -export function provideDatadogRouter(): EnvironmentProviders { - return makeEnvironmentProviders([ - { - // Needed for Angular v15 support (provideEnvironmentInitializer requires v16+) - provide: ENVIRONMENT_INITIALIZER, - multi: true, - useFactory: () => { - const router = inject(Router) - - return () => { - let currentPath: string | undefined - - // No unsubscribe needed as its for the full app lifecycle and because DestroyRef requires v16+ - router.events - .pipe(filter((event): event is GuardsCheckEnd => event instanceof GuardsCheckEnd)) - .subscribe((event) => { - if (!event.shouldActivate) { - return - } - - const url = event.urlAfterRedirects - const path = url.replace(/[?#].*$/, '') - - if (path === currentPath) { - return - } - currentPath = path - - const root = event.state.root - startAngularView(root, url) - }) - } - }, - }, - ]) -} diff --git a/packages/rum-angular/src/domain/angularRouter/startAngularView.spec.ts b/packages/rum-angular/src/domain/angularRouter/startAngularView.spec.ts deleted file mode 100644 index 3610a25d89..0000000000 --- a/packages/rum-angular/src/domain/angularRouter/startAngularView.spec.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { computeViewName } from './startAngularView' -import type { RouteSnapshot } from './types' - -function createSnapshot( - path: string | undefined, - children: RouteSnapshot[] = [], - outlet: string = 'primary', - url: Array<{ path: string }> = [] -): RouteSnapshot { - return { - routeConfig: path !== undefined ? { path } : null, - children, - outlet, - url, - } -} - -/** - * Build a RouteSnapshot tree from a compact route string and an actual path. - * - * Route string format mirrors the React Router test convention: - * 'foo > bar > :id' represents nested routes { path: 'foo', children: [{ path: 'bar', children: [{ path: ':id' }] }] } - * - * The actualPath is used to compute the URL segments matched by '**' wildcards, - * since Angular stores the matched segments on the wildcard node's `url` property. - */ -function buildSnapshot(routePaths: string, actualPath: string): RouteSnapshot { - const paths = routePaths.split(' > ') - - // Compute prefix consumed before the wildcard to derive matched URL segments - const wildcardIndex = paths.indexOf('**') - let wildcardUrl: Array<{ path: string }> = [] - if (wildcardIndex !== -1) { - const prefixSegments = paths.slice(0, wildcardIndex).filter((p) => p !== '') - let remaining = actualPath.startsWith('/') ? actualPath.slice(1) : actualPath - for (const prefix of prefixSegments) { - if (remaining.startsWith(prefix)) { - remaining = remaining.slice(prefix.length) - if (remaining.startsWith('/')) { - remaining = remaining.slice(1) - } - } - } - wildcardUrl = remaining ? remaining.split('/').map((s) => ({ path: s })) : [] - } - - // Build snapshot chain from right to left - let current: RouteSnapshot | undefined - for (let i = paths.length - 1; i >= 0; i--) { - const path = paths[i] - const url = path === '**' ? wildcardUrl : [] - current = createSnapshot(path, current ? [current] : [], 'primary', url) - } - - // Wrap in root node (null routeConfig), matching Angular's ActivatedRouteSnapshot tree - return createSnapshot(undefined, current ? [current] : []) -} - -describe('computeViewName', () => { - it('returns / when root has no children', () => { - expect(computeViewName(createSnapshot(undefined))).toBe('/') - }) - - it('follows primary outlet only, ignoring named outlets', () => { - const primaryChild = createSnapshot('users', [], 'primary') - const namedChild = createSnapshot('sidebar', [], 'sidebar') - const root = createSnapshot(undefined, [namedChild, primaryChild]) - expect(computeViewName(root)).toBe('/users') - }) - - // prettier-ignore - const cases = [ - // route paths, actual path, expected view name - - // Simple paths - ['', '/', '/'], - ['users', '/users', '/users'], - ['users > :id', '/users/42', '/users/:id'], - ['users > :id > posts > :postId','/users/1/posts/2', '/users/:id/posts/:postId'], - ['users/list', '/users/list', '/users/list'], - - // Empty-path wrappers (layout routes) - [' > users > :id', '/users/42', '/users/:id'], - [' > > users', '/users', '/users'], - - // Lazy-loaded routes (same shape post-resolution) - ['admin > settings', '/admin/settings', '/admin/settings'], - - // Wildcards - ['**', '/foo/bar', '/foo/bar'], - ['**', '/', '/'], - ['admin > **', '/admin/foo', '/admin/foo'], - ] as const - - cases.forEach(([routePaths, path, expectedViewName]) => { - it(`returns "${expectedViewName}" for path "${path}" with routes "${routePaths}"`, () => { - const root = buildSnapshot(routePaths, path) - expect(computeViewName(root)).toBe(expectedViewName) - }) - }) -}) diff --git a/packages/rum-angular/src/domain/angularRouter/startAngularView.ts b/packages/rum-angular/src/domain/angularRouter/startAngularView.ts deleted file mode 100644 index 902841a346..0000000000 --- a/packages/rum-angular/src/domain/angularRouter/startAngularView.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { display } from '@datadog/browser-core' -import { onRumInit } from '../angularPlugin' -import type { RouteSnapshot } from './types' - -const PRIMARY_OUTLET = 'primary' - -export function startAngularView(root: RouteSnapshot, url: string) { - onRumInit((configuration, rumPublicApi) => { - if (!configuration.router) { - display.warn('`router: true` is missing from the react plugin configuration, the view will not be tracked.') - return - } - - const viewName = computeViewName(root) - - rumPublicApi.startView({ name: viewName, url }) - }) -} - -export function computeViewName(root: RouteSnapshot): string { - const segments: string[] = [] - let current: RouteSnapshot | undefined = root - - while (current) { - const path = current.routeConfig?.path - - if (path === '**') { - for (const segment of current.url) { - segments.push(segment.path) - } - break - } - - if (path) { - segments.push(path) - } - - // Follow primary outlet only — named outlets (e.g. sidebar) are excluded - current = current.children.find((child) => child.outlet === PRIMARY_OUTLET) - } - - return `/${segments.join('/')}` -} diff --git a/packages/rum-angular/src/domain/angularRouter/types.ts b/packages/rum-angular/src/domain/angularRouter/types.ts deleted file mode 100644 index 91834045e8..0000000000 --- a/packages/rum-angular/src/domain/angularRouter/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Minimal interface matching the shape of Angular's ActivatedRouteSnapshot - * that we need for view name computation. Avoids importing @angular/router at runtime. - */ -export interface RouteSnapshot { - routeConfig: { path?: string } | null - children: RouteSnapshot[] - outlet: string - url: Array<{ path: string }> -} diff --git a/packages/rum-angular/src/domain/error/addAngularError.spec.ts b/packages/rum-angular/src/domain/error/addAngularError.spec.ts deleted file mode 100644 index a5a5a3879c..0000000000 --- a/packages/rum-angular/src/domain/error/addAngularError.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { initializeAngularPlugin } from '../../../test/initializeAngularPlugin' -import { addAngularError } from './addAngularError' - -describe('addAngularError', () => { - it('delegates the error to addError', () => { - const addErrorSpy = jasmine.createSpy() - initializeAngularPlugin({ - addError: addErrorSpy, - }) - const originalError = new Error('error message') - - addAngularError(originalError) - - expect(addErrorSpy).toHaveBeenCalledOnceWith({ - error: originalError, - handlingStack: jasmine.any(String), - startClocks: jasmine.any(Object), - context: { - framework: 'angular', - }, - }) - }) - - it('should merge dd_context from the original error with angular error context', () => { - const addErrorSpy = jasmine.createSpy() - initializeAngularPlugin({ - addError: addErrorSpy, - }) - const originalError = new Error('error message') - ;(originalError as any).dd_context = { component: 'UserList', param: 42 } - - addAngularError(originalError) - - expect(addErrorSpy).toHaveBeenCalledWith( - jasmine.objectContaining({ - error: originalError, - context: { - framework: 'angular', - component: 'UserList', - param: 42, - }, - }) - ) - }) - - it('handles non-Error values', () => { - const addErrorSpy = jasmine.createSpy() - initializeAngularPlugin({ - addError: addErrorSpy, - }) - - addAngularError('string error') - - expect(addErrorSpy).toHaveBeenCalledOnceWith( - jasmine.objectContaining({ - error: 'string error', - handlingStack: jasmine.any(String), - startClocks: jasmine.any(Object), - context: { framework: 'angular' }, - }) - ) - }) -}) diff --git a/packages/rum-angular/src/domain/error/addAngularError.ts b/packages/rum-angular/src/domain/error/addAngularError.ts deleted file mode 100644 index 17c610c6db..0000000000 --- a/packages/rum-angular/src/domain/error/addAngularError.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { Context } from '@datadog/browser-core' -import { callMonitored, clocksNow, createHandlingStack } from '@datadog/browser-core' -import { onRumStart } from '../angularPlugin' - -/** - * Add an Angular error to the RUM session. - * - * This function is used internally by `provideDatadogErrorHandler()`, but can also be called - * directly to report errors caught by custom error handling logic. - * - * @category Error - * @example - * ```ts - * import { addAngularError } from '@datadog/browser-rum-angular' - * - * // In a custom ErrorHandler - * handleError(error: any) { - * addAngularError(error) - * // your own error handling... - * } - * ``` - */ -export function addAngularError(error: unknown) { - const handlingStack = createHandlingStack('angular error') - const startClocks = clocksNow() - onRumStart((addError) => { - callMonitored(() => { - addError({ - error, - handlingStack, - startClocks, - context: { - ...(typeof error === 'object' && error !== null ? (error as { dd_context?: Context }).dd_context : undefined), - framework: 'angular', - }, - }) - }) - }) -} diff --git a/packages/rum-angular/src/domain/error/index.ts b/packages/rum-angular/src/domain/error/index.ts deleted file mode 100644 index e20b7ee212..0000000000 --- a/packages/rum-angular/src/domain/error/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { addAngularError } from './addAngularError' -export { provideDatadogErrorHandler } from './provideDatadogErrorHandler' diff --git a/packages/rum-angular/src/domain/error/provideDatadogErrorHandler.spec.ts b/packages/rum-angular/src/domain/error/provideDatadogErrorHandler.spec.ts deleted file mode 100644 index 7118728b34..0000000000 --- a/packages/rum-angular/src/domain/error/provideDatadogErrorHandler.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { EnvironmentInjector } from '@angular/core' -import { ErrorHandler, Injector, createEnvironmentInjector } from '@angular/core' -import { initializeAngularPlugin } from '../../../test/initializeAngularPlugin' -import { provideDatadogErrorHandler } from './provideDatadogErrorHandler' - -function createErrorHandler(): ErrorHandler { - const injector = createEnvironmentInjector([provideDatadogErrorHandler()], Injector.NULL as EnvironmentInjector) - return injector.get(ErrorHandler) -} - -describe('provideDatadogErrorHandler', () => { - it('provides an ErrorHandler that reports errors to Datadog', () => { - const addErrorSpy = jasmine.createSpy() - initializeAngularPlugin({ addError: addErrorSpy }) - - spyOn(console, 'error') // Mute console.errors - const handler = createErrorHandler() - handler.handleError(new Error('test error')) - - expect(addErrorSpy).toHaveBeenCalled() - }) - - it('still logs the error to the console via default ErrorHandler', () => { - initializeAngularPlugin() - - const consoleErrorSpy = spyOn(console, 'error') - const handler = createErrorHandler() - handler.handleError(new Error('test error')) - - expect(consoleErrorSpy).toHaveBeenCalled() - }) -}) diff --git a/packages/rum-angular/src/domain/error/provideDatadogErrorHandler.ts b/packages/rum-angular/src/domain/error/provideDatadogErrorHandler.ts deleted file mode 100644 index a1de66c28a..0000000000 --- a/packages/rum-angular/src/domain/error/provideDatadogErrorHandler.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { EnvironmentProviders } from '@angular/core' -import { ErrorHandler, makeEnvironmentProviders } from '@angular/core' -import { addAngularError } from './addAngularError' - -// eslint-disable-next-line no-restricted-syntax -class DatadogErrorHandler extends ErrorHandler { - override handleError(error: unknown): void { - addAngularError(error) - super.handleError(error) - } -} - -/** - * Provides a Datadog-instrumented Angular ErrorHandler that reports errors to RUM. - * - * @category Error - * @example - * ```ts - * import { provideDatadogErrorHandler } from '@datadog/browser-rum-angular' - * - * bootstrapApplication(AppComponent, { - * providers: [provideDatadogErrorHandler()], - * }) - * ``` - */ -export function provideDatadogErrorHandler(): EnvironmentProviders { - return makeEnvironmentProviders([{ provide: ErrorHandler, useClass: DatadogErrorHandler }]) -} diff --git a/packages/rum-angular/src/entries/main.ts b/packages/rum-angular/src/entries/main.ts deleted file mode 100644 index 2fd1b9dd44..0000000000 --- a/packages/rum-angular/src/entries/main.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { angularPlugin } from '../domain/angularPlugin' -export type { AngularPluginConfiguration } from '../domain/angularPlugin' -export { provideDatadogRouter } from '../domain/angularRouter/provideDatadogRouter' -export { addAngularError, provideDatadogErrorHandler } from '../domain/error' diff --git a/packages/rum-angular/test/initializeAngularPlugin.ts b/packages/rum-angular/test/initializeAngularPlugin.ts deleted file mode 100644 index 47522d437b..0000000000 --- a/packages/rum-angular/test/initializeAngularPlugin.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { RumInitConfiguration, RumPublicApi, StartRumResult } from '@datadog/browser-rum-core' -import { noop } from '@datadog/browser-core' -import { angularPlugin, resetAngularPlugin } from '../src/domain/angularPlugin' -import { registerCleanupTask } from '../../core/test' - -export function initializeAngularPlugin({ - addError = noop, -}: { - addError?: StartRumResult['addError'] -} = {}) { - resetAngularPlugin() - const plugin = angularPlugin() - - plugin.onInit!({ - publicApi: {} as RumPublicApi, - initConfiguration: {} as RumInitConfiguration, - }) - plugin.onRumStart!({ addError }) - - registerCleanupTask(() => { - resetAngularPlugin() - }) -} diff --git a/packages/rum-angular/tsconfig.json b/packages/rum-angular/tsconfig.json deleted file mode 100644 index c27e50b613..0000000000 --- a/packages/rum-angular/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "types": ["jasmine"], - "allowImportingTsExtensions": true, - "noEmit": true - }, - "include": ["src", "test"] -} diff --git a/scripts/build/build-test-apps.ts b/scripts/build/build-test-apps.ts index 1135abc88a..0fecce130c 100644 --- a/scripts/build/build-test-apps.ts +++ b/scripts/build/build-test-apps.ts @@ -26,7 +26,6 @@ const APPS: AppConfig[] = [ { name: 'react-shopist-like' }, { name: 'microfrontend' }, { name: 'nextjs' }, - { name: 'angular-app' }, { name: 'vue-router-app' }, // React Router apps diff --git a/test/apps/angular-app/.gitignore b/test/apps/angular-app/.gitignore deleted file mode 100644 index e20e3cfac7..0000000000 --- a/test/apps/angular-app/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -dist -node_modules -.yarn/* diff --git a/test/apps/angular-app/main.ts b/test/apps/angular-app/main.ts deleted file mode 100644 index 9e74db2d7c..0000000000 --- a/test/apps/angular-app/main.ts +++ /dev/null @@ -1,100 +0,0 @@ -import 'zone.js' -import { Component } from '@angular/core' -import { bootstrapApplication } from '@angular/platform-browser' -import { provideRouter, RouterOutlet, RouterLink, type Routes } from '@angular/router' -import { datadogRum } from '@datadog/browser-rum' -import { angularPlugin, provideDatadogRouter, provideDatadogErrorHandler } from '@datadog/browser-rum-angular' - -declare global { - interface Window { - RUM_CONFIGURATION?: any - RUM_CONTEXT?: any - } -} - -// Initialize RUM before bootstrap -datadogRum.init({ ...window.RUM_CONFIGURATION, plugins: [angularPlugin({ router: true })] }) -if (window.RUM_CONTEXT) { - datadogRum.setGlobalContext(window.RUM_CONTEXT) -} - -// Components - -@Component({ - selector: 'app-initial-route', - standalone: true, - imports: [RouterLink], - template: ` -

Initial Route

- Go to Parameterized Route
- Go to Nested Route
- Go to Wildcard Route
- Change Query Param
- - - `, -}) -class InitialRouteComponent { - throwError() { - throw new Error('angular error from component') - } - - throwErrorWithContext() { - const error = new Error('angular error with dd_context') - ;(error as any).dd_context = { component: 'InitialRoute', userId: 42 } - throw error - } -} - -@Component({ - selector: 'app-parameterized-route', - standalone: true, - template: '

Parameterized Route

', -}) -class ParameterizedRouteComponent {} - -@Component({ - selector: 'app-nested-route', - standalone: true, - template: '

Nested Route

', -}) -class NestedRouteComponent {} - -@Component({ - selector: 'app-wildcard-route', - standalone: true, - template: '

Wildcard Route

', -}) -class WildcardRouteComponent {} - -@Component({ - selector: 'app-root', - standalone: true, - imports: [RouterOutlet, RouterLink], - template: ` - - - `, -}) -class AppComponent {} - -// Routes -const nestedRoutes: Routes = [{ path: 'nested', component: NestedRouteComponent }] - -const routes: Routes = [ - { path: '', component: InitialRouteComponent }, - { path: 'parameterized/:id', component: ParameterizedRouteComponent }, - { path: 'parent', loadChildren: () => Promise.resolve(nestedRoutes) }, - { path: '**', component: WildcardRouteComponent }, -] - -// Bootstrap - dynamically create root element (E2E framework serves bare HTML) -const rootElement = document.createElement('app-root') -document.body.appendChild(rootElement) - -void bootstrapApplication(AppComponent, { - providers: [provideRouter(routes), provideDatadogRouter(), provideDatadogErrorHandler()], -}) diff --git a/test/apps/angular-app/package.json b/test/apps/angular-app/package.json deleted file mode 100644 index 7906767055..0000000000 --- a/test/apps/angular-app/package.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "angular-app", - "private": true, - "scripts": { - "build": "webpack" - }, - "dependencies": { - "@angular/common": "15.2.10", - "@angular/compiler": "15.2.10", - "@angular/compiler-cli": "15.2.10", - "@angular/core": "15.2.10", - "@angular/platform-browser": "15.2.10", - "@angular/router": "15.2.10", - "rxjs": "7.8.2", - "zone.js": "0.12.0" - }, - "peerDependencies": { - "@datadog/browser-rum": "*", - "@datadog/browser-rum-angular": "*" - }, - "peerDependenciesMeta": { - "@datadog/browser-rum": { - "optional": true - }, - "@datadog/browser-rum-angular": { - "optional": true - } - }, - "resolutions": { - "@datadog/browser-rum-core": "file:../../../packages/rum-core/package.tgz", - "@datadog/browser-core": "file:../../../packages/core/package.tgz", - "@datadog/browser-rum": "file:../../../packages/rum/package.tgz", - "@datadog/browser-rum-angular": "file:../../../packages/rum-angular/package.tgz", - "@datadog/browser-rum-slim": "file:../../../packages/rum-slim/package.tgz", - "@datadog/browser-worker": "file:../../../packages/worker/package.tgz" - }, - "devDependencies": { - "@ngtools/webpack": "15.2.11", - "babel-loader": "9.2.1", - "typescript": "4.9.5", - "webpack": "5.105.2", - "webpack-cli": "6.0.1" - }, - "volta": { - "extends": "../../../package.json" - } -} diff --git a/test/apps/angular-app/tsconfig.json b/test/apps/angular-app/tsconfig.json deleted file mode 100644 index be972b534e..0000000000 --- a/test/apps/angular-app/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "outDir": "./dist/", - "strict": true, - "module": "es2022", - "moduleResolution": "node", - "esModuleInterop": true, - "target": "es2022", - "lib": ["ES2022", "DOM"], - "types": [], - "experimentalDecorators": true, - "useDefineForClassFields": false - }, - "angularCompilerOptions": { - "enableI18nLegacyMessageIdFormat": false, - "strictInjectionParameters": true, - "strictInputAccessModifiers": true, - "strictTemplates": true - } -} diff --git a/test/apps/angular-app/webpack.config.js b/test/apps/angular-app/webpack.config.js deleted file mode 100644 index 165136d566..0000000000 --- a/test/apps/angular-app/webpack.config.js +++ /dev/null @@ -1,43 +0,0 @@ -const path = require('node:path') -// eslint-disable-next-line import/no-unresolved -const { AngularWebpackPlugin } = require('@ngtools/webpack') - -module.exports = { - mode: 'production', - entry: './main.ts', - target: ['web', 'es2022'], - module: { - rules: [ - { test: /\.ts$/, use: '@ngtools/webpack' }, - { - test: /\.[cm]?js$/, - include: /node_modules/, - use: { - loader: 'babel-loader', - options: { - plugins: ['@angular/compiler-cli/linker/babel'], - compact: false, - cacheDirectory: true, - }, - }, - }, - ], - }, - resolve: { - extensions: ['.ts', '.js'], - }, - plugins: [ - new AngularWebpackPlugin({ - tsconfig: path.resolve(__dirname, 'tsconfig.json'), - jitMode: false, - }), - ], - optimization: { - chunkIds: 'named', - }, - output: { - path: path.resolve(__dirname, 'dist'), - filename: 'angular-app.js', - chunkFilename: 'chunks/[name]-[contenthash]-angular-app.js', - }, -} diff --git a/test/apps/angular-app/yarn.lock b/test/apps/angular-app/yarn.lock deleted file mode 100644 index 7070b9c168..0000000000 --- a/test/apps/angular-app/yarn.lock +++ /dev/null @@ -1,2491 +0,0 @@ -# This file is generated by running "yarn install" inside your project. -# Manual changes might be lost - proceed with caution! - -__metadata: - version: 8 - cacheKey: 10c0 - -"@ampproject/remapping@npm:^2.1.0": - version: 2.3.0 - resolution: "@ampproject/remapping@npm:2.3.0" - dependencies: - "@jridgewell/gen-mapping": "npm:^0.3.5" - "@jridgewell/trace-mapping": "npm:^0.3.24" - checksum: 10c0/81d63cca5443e0f0c72ae18b544cc28c7c0ec2cea46e7cb888bb0e0f411a1191d0d6b7af798d54e30777d8d1488b2ec0732aac2be342d3d7d3ffd271c6f489ed - languageName: node - linkType: hard - -"@angular/common@npm:15.2.10": - version: 15.2.10 - resolution: "@angular/common@npm:15.2.10" - dependencies: - tslib: "npm:^2.3.0" - peerDependencies: - "@angular/core": 15.2.10 - rxjs: ^6.5.3 || ^7.4.0 - checksum: 10c0/3c473cf2d4124c1f8cb6119bc88ef891e6cbf8a00058b09c80f506742285da579480172b7b82e33909aca27ba2bd14adfcf42bdb92cbd864629fa1b40fd9b099 - languageName: node - linkType: hard - -"@angular/compiler-cli@npm:15.2.10": - version: 15.2.10 - resolution: "@angular/compiler-cli@npm:15.2.10" - dependencies: - "@babel/core": "npm:7.19.3" - "@jridgewell/sourcemap-codec": "npm:^1.4.14" - chokidar: "npm:^3.0.0" - convert-source-map: "npm:^1.5.1" - dependency-graph: "npm:^0.11.0" - magic-string: "npm:^0.27.0" - reflect-metadata: "npm:^0.1.2" - semver: "npm:^7.0.0" - tslib: "npm:^2.3.0" - yargs: "npm:^17.2.1" - peerDependencies: - "@angular/compiler": 15.2.10 - typescript: ">=4.8.2 <5.0" - bin: - ng-xi18n: bundles/src/bin/ng_xi18n.js - ngc: bundles/src/bin/ngc.js - ngcc: bundles/ngcc/main-ngcc.js - checksum: 10c0/b9bce00c178ba75edb590e34965a0b0fd1def03cd83c167a98dbb48e6b6380f053951335bd60f67fc6668bed3c7dce2da4a912bfbc96a2030e626cc992268bfd - languageName: node - linkType: hard - -"@angular/compiler@npm:15.2.10": - version: 15.2.10 - resolution: "@angular/compiler@npm:15.2.10" - dependencies: - tslib: "npm:^2.3.0" - peerDependencies: - "@angular/core": 15.2.10 - peerDependenciesMeta: - "@angular/core": - optional: true - checksum: 10c0/9b9c8d7263d647197e44c57a1852f4900e9c3c8f52e69f8bd002c63569f99580e9ec3c5fcaa0ceb3b3fd666661d129c45bcf769b32c28d07ae1ba6bd435a1038 - languageName: node - linkType: hard - -"@angular/core@npm:15.2.10": - version: 15.2.10 - resolution: "@angular/core@npm:15.2.10" - dependencies: - tslib: "npm:^2.3.0" - peerDependencies: - rxjs: ^6.5.3 || ^7.4.0 - zone.js: ~0.11.4 || ~0.12.0 || ~0.13.0 - checksum: 10c0/d0c3c4e97ac56867b4459f9299571442a4a928c2b7a2bec6018ed242dee323b7f470abf9272edb16b6e60dd3c8de11ee2596662bcd08cd87e852ecb0abbee52e - languageName: node - linkType: hard - -"@angular/platform-browser@npm:15.2.10": - version: 15.2.10 - resolution: "@angular/platform-browser@npm:15.2.10" - dependencies: - tslib: "npm:^2.3.0" - peerDependencies: - "@angular/animations": 15.2.10 - "@angular/common": 15.2.10 - "@angular/core": 15.2.10 - peerDependenciesMeta: - "@angular/animations": - optional: true - checksum: 10c0/1a24ebd9bd1685f6b518269146f0f232b863e4ad17aa0f05d5d83e8b3cd7b86e3eb780104c3f2ba4108035ac8731608348dc6a458d59542cb9ce0fec5d248728 - languageName: node - linkType: hard - -"@angular/router@npm:15.2.10": - version: 15.2.10 - resolution: "@angular/router@npm:15.2.10" - dependencies: - tslib: "npm:^2.3.0" - peerDependencies: - "@angular/common": 15.2.10 - "@angular/core": 15.2.10 - "@angular/platform-browser": 15.2.10 - rxjs: ^6.5.3 || ^7.4.0 - checksum: 10c0/8f36f3da6870725123f05ae8060d9d8c650a494555a707a460fecf9913f10692f87f12a5b458cd0ca2189887c6a643b8b87983db8b936914672246a3b55732e1 - languageName: node - linkType: hard - -"@babel/code-frame@npm:^7.18.6, @babel/code-frame@npm:^7.28.6, @babel/code-frame@npm:^7.29.0": - version: 7.29.0 - resolution: "@babel/code-frame@npm:7.29.0" - dependencies: - "@babel/helper-validator-identifier": "npm:^7.28.5" - js-tokens: "npm:^4.0.0" - picocolors: "npm:^1.1.1" - checksum: 10c0/d34cc504e7765dfb576a663d97067afb614525806b5cad1a5cc1a7183b916fec8ff57fa233585e3926fd5a9e6b31aae6df91aa81ae9775fb7a28f658d3346f0d - languageName: node - linkType: hard - -"@babel/compat-data@npm:^7.28.6": - version: 7.29.0 - resolution: "@babel/compat-data@npm:7.29.0" - checksum: 10c0/08f348554989d23aa801bf1405aa34b15e841c0d52d79da7e524285c77a5f9d298e70e11d91cc578d8e2c9542efc586d50c5f5cf8e1915b254a9dcf786913a94 - languageName: node - linkType: hard - -"@babel/core@npm:7.19.3": - version: 7.19.3 - resolution: "@babel/core@npm:7.19.3" - dependencies: - "@ampproject/remapping": "npm:^2.1.0" - "@babel/code-frame": "npm:^7.18.6" - "@babel/generator": "npm:^7.19.3" - "@babel/helper-compilation-targets": "npm:^7.19.3" - "@babel/helper-module-transforms": "npm:^7.19.0" - "@babel/helpers": "npm:^7.19.0" - "@babel/parser": "npm:^7.19.3" - "@babel/template": "npm:^7.18.10" - "@babel/traverse": "npm:^7.19.3" - "@babel/types": "npm:^7.19.3" - convert-source-map: "npm:^1.7.0" - debug: "npm:^4.1.0" - gensync: "npm:^1.0.0-beta.2" - json5: "npm:^2.2.1" - semver: "npm:^6.3.0" - checksum: 10c0/2ef6bc3c407f5aa868a3fdc5ec58bcaf98d073de5fff65c1b16b1133cd232f43b5a413a1356c4cdd37f477fb006ac9fc0d5fce8a0f2f4f5d881de0dd1f6b0b06 - languageName: node - linkType: hard - -"@babel/generator@npm:^7.19.3, @babel/generator@npm:^7.29.0": - version: 7.29.1 - resolution: "@babel/generator@npm:7.29.1" - dependencies: - "@babel/parser": "npm:^7.29.0" - "@babel/types": "npm:^7.29.0" - "@jridgewell/gen-mapping": "npm:^0.3.12" - "@jridgewell/trace-mapping": "npm:^0.3.28" - jsesc: "npm:^3.0.2" - checksum: 10c0/349086e6876258ef3fb2823030fee0f6c0eb9c3ebe35fc572e16997f8c030d765f636ddc6299edae63e760ea6658f8ee9a2edfa6d6b24c9a80c917916b973551 - languageName: node - linkType: hard - -"@babel/helper-compilation-targets@npm:^7.19.3": - version: 7.28.6 - resolution: "@babel/helper-compilation-targets@npm:7.28.6" - dependencies: - "@babel/compat-data": "npm:^7.28.6" - "@babel/helper-validator-option": "npm:^7.27.1" - browserslist: "npm:^4.24.0" - lru-cache: "npm:^5.1.1" - semver: "npm:^6.3.1" - checksum: 10c0/3fcdf3b1b857a1578e99d20508859dbd3f22f3c87b8a0f3dc540627b4be539bae7f6e61e49d931542fe5b557545347272bbdacd7f58a5c77025a18b745593a50 - languageName: node - linkType: hard - -"@babel/helper-globals@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/helper-globals@npm:7.28.0" - checksum: 10c0/5a0cd0c0e8c764b5f27f2095e4243e8af6fa145daea2b41b53c0c1414fe6ff139e3640f4e2207ae2b3d2153a1abd346f901c26c290ee7cb3881dd922d4ee9232 - languageName: node - linkType: hard - -"@babel/helper-module-imports@npm:^7.28.6": - version: 7.28.6 - resolution: "@babel/helper-module-imports@npm:7.28.6" - dependencies: - "@babel/traverse": "npm:^7.28.6" - "@babel/types": "npm:^7.28.6" - checksum: 10c0/b49d8d8f204d9dbfd5ac70c54e533e5269afb3cea966a9d976722b13e9922cc773a653405f53c89acb247d5aebdae4681d631a3ae3df77ec046b58da76eda2ac - languageName: node - linkType: hard - -"@babel/helper-module-transforms@npm:^7.19.0": - version: 7.28.6 - resolution: "@babel/helper-module-transforms@npm:7.28.6" - dependencies: - "@babel/helper-module-imports": "npm:^7.28.6" - "@babel/helper-validator-identifier": "npm:^7.28.5" - "@babel/traverse": "npm:^7.28.6" - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 10c0/6f03e14fc30b287ce0b839474b5f271e72837d0cafe6b172d759184d998fbee3903a035e81e07c2c596449e504f453463d58baa65b6f40a37ded5bec74620b2b - languageName: node - linkType: hard - -"@babel/helper-string-parser@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/helper-string-parser@npm:7.27.1" - checksum: 10c0/8bda3448e07b5583727c103560bcf9c4c24b3c1051a4c516d4050ef69df37bb9a4734a585fe12725b8c2763de0a265aa1e909b485a4e3270b7cfd3e4dbe4b602 - languageName: node - linkType: hard - -"@babel/helper-validator-identifier@npm:^7.28.5": - version: 7.28.5 - resolution: "@babel/helper-validator-identifier@npm:7.28.5" - checksum: 10c0/42aaebed91f739a41f3d80b72752d1f95fd7c72394e8e4bd7cdd88817e0774d80a432451bcba17c2c642c257c483bf1d409dd4548883429ea9493a3bc4ab0847 - languageName: node - linkType: hard - -"@babel/helper-validator-option@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/helper-validator-option@npm:7.27.1" - checksum: 10c0/6fec5f006eba40001a20f26b1ef5dbbda377b7b68c8ad518c05baa9af3f396e780bdfded24c4eef95d14bb7b8fd56192a6ed38d5d439b97d10efc5f1a191d148 - languageName: node - linkType: hard - -"@babel/helpers@npm:^7.19.0": - version: 7.29.2 - resolution: "@babel/helpers@npm:7.29.2" - dependencies: - "@babel/template": "npm:^7.28.6" - "@babel/types": "npm:^7.29.0" - checksum: 10c0/dab0e65b9318b2502a62c58bc0913572318595eec0482c31f0ad416b72636e6698a1d7c57cd2791d4528eb8c548bca88d338dc4d2a55a108dc1f6702f9bc5512 - languageName: node - linkType: hard - -"@babel/parser@npm:^7.19.3": - version: 7.29.2 - resolution: "@babel/parser@npm:7.29.2" - dependencies: - "@babel/types": "npm:^7.29.0" - bin: - parser: ./bin/babel-parser.js - checksum: 10c0/e5a4e69e3ac7acdde995f37cf299a68458cfe7009dff66bd0962fd04920bef287201169006af365af479c08ff216bfefbb595e331f87f6ae7283858aebbc3317 - languageName: node - linkType: hard - -"@babel/parser@npm:^7.28.6, @babel/parser@npm:^7.29.0": - version: 7.29.0 - resolution: "@babel/parser@npm:7.29.0" - dependencies: - "@babel/types": "npm:^7.29.0" - bin: - parser: ./bin/babel-parser.js - checksum: 10c0/333b2aa761264b91577a74bee86141ef733f9f9f6d4fc52548e4847dc35dfbf821f58c46832c637bfa761a6d9909d6a68f7d1ed59e17e4ffbb958dc510c17b62 - languageName: node - linkType: hard - -"@babel/template@npm:^7.18.10, @babel/template@npm:^7.28.6": - version: 7.28.6 - resolution: "@babel/template@npm:7.28.6" - dependencies: - "@babel/code-frame": "npm:^7.28.6" - "@babel/parser": "npm:^7.28.6" - "@babel/types": "npm:^7.28.6" - checksum: 10c0/66d87225ed0bc77f888181ae2d97845021838c619944877f7c4398c6748bcf611f216dfd6be74d39016af502bca876e6ce6873db3c49e4ac354c56d34d57e9f5 - languageName: node - linkType: hard - -"@babel/traverse@npm:^7.19.3, @babel/traverse@npm:^7.28.6": - version: 7.29.0 - resolution: "@babel/traverse@npm:7.29.0" - dependencies: - "@babel/code-frame": "npm:^7.29.0" - "@babel/generator": "npm:^7.29.0" - "@babel/helper-globals": "npm:^7.28.0" - "@babel/parser": "npm:^7.29.0" - "@babel/template": "npm:^7.28.6" - "@babel/types": "npm:^7.29.0" - debug: "npm:^4.3.1" - checksum: 10c0/f63ef6e58d02a9fbf3c0e2e5f1c877da3e0bc57f91a19d2223d53e356a76859cbaf51171c9211c71816d94a0e69efa2732fd27ffc0e1bbc84b636e60932333eb - languageName: node - linkType: hard - -"@babel/types@npm:^7.19.3, @babel/types@npm:^7.28.6, @babel/types@npm:^7.29.0": - version: 7.29.0 - resolution: "@babel/types@npm:7.29.0" - dependencies: - "@babel/helper-string-parser": "npm:^7.27.1" - "@babel/helper-validator-identifier": "npm:^7.28.5" - checksum: 10c0/23cc3466e83bcbfab8b9bd0edaafdb5d4efdb88b82b3be6728bbade5ba2f0996f84f63b1c5f7a8c0d67efded28300898a5f930b171bb40b311bca2029c4e9b4f - languageName: node - linkType: hard - -"@datadog/browser-core@file:../../../packages/core/package.tgz::locator=angular-app%40workspace%3A.": - version: 6.31.0 - resolution: "@datadog/browser-core@file:../../../packages/core/package.tgz#../../../packages/core/package.tgz::hash=6201e3&locator=angular-app%40workspace%3A." - checksum: 10c0/a019713374971a2dd35c63318cecd76e38d08348a24ca5b88abb8fca8501d17589e187bd4e68c7358f3523dd3fd08d110b307647854a620b0f3371f2c7db14ca - languageName: node - linkType: hard - -"@datadog/browser-rum-angular@file:../../../packages/rum-angular/package.tgz::locator=angular-app%40workspace%3A.": - version: 0.0.0 - resolution: "@datadog/browser-rum-angular@file:../../../packages/rum-angular/package.tgz#../../../packages/rum-angular/package.tgz::hash=004229&locator=angular-app%40workspace%3A." - dependencies: - "@datadog/browser-core": "npm:6.31.0" - "@datadog/browser-rum-core": "npm:6.31.0" - peerDependencies: - "@angular/core": ">=15 <=21" - "@angular/router": ">=15 <=21" - rxjs: ">=7" - checksum: 10c0/ec9054d51805f45cb4618bcbc6fa00049ba5d1597048aa3c354aea14a15a06275ee0ba06b3de7d77fbd0d2f009f74159a88e10f2f6b9e629eeed0a5ca30a27ae - languageName: node - linkType: hard - -"@datadog/browser-rum-core@file:../../../packages/rum-core/package.tgz::locator=angular-app%40workspace%3A.": - version: 6.31.0 - resolution: "@datadog/browser-rum-core@file:../../../packages/rum-core/package.tgz#../../../packages/rum-core/package.tgz::hash=0334b6&locator=angular-app%40workspace%3A." - dependencies: - "@datadog/browser-core": "npm:6.31.0" - checksum: 10c0/3ece55e0ca0bc0691fd91c557433c806e2871b56e3a0008c01b6e369809c9130fa384952b22ee6464d49d182d3e4e5bd66f51b6b629d4024d6e482f7e64d13c8 - languageName: node - linkType: hard - -"@datadog/browser-rum@file:../../../packages/rum/package.tgz::locator=angular-app%40workspace%3A.": - version: 6.31.0 - resolution: "@datadog/browser-rum@file:../../../packages/rum/package.tgz#../../../packages/rum/package.tgz::hash=033900&locator=angular-app%40workspace%3A." - dependencies: - "@datadog/browser-core": "npm:6.31.0" - "@datadog/browser-rum-core": "npm:6.31.0" - peerDependencies: - "@datadog/browser-logs": 6.31.0 - peerDependenciesMeta: - "@datadog/browser-logs": - optional: true - checksum: 10c0/ac0080efb7f664a5664162a64e618b885d2c4d2d06426db0ad076c855dd02cbc84f1ba6fc30913db8db74d50a511d0a0a62ba1ad7bb9e8de5c3ef0c03e3f9c56 - languageName: node - linkType: hard - -"@discoveryjs/json-ext@npm:^0.6.1": - version: 0.6.3 - resolution: "@discoveryjs/json-ext@npm:0.6.3" - checksum: 10c0/778a9f9d5c3696da3c1f9fa4186613db95a1090abbfb6c2601430645c0d0158cd5e4ba4f32c05904e2dd2747d57710f6aab22bd2f8aa3c4e8feab9b247c65d85 - languageName: node - linkType: hard - -"@gar/promise-retry@npm:^1.0.0": - version: 1.0.2 - resolution: "@gar/promise-retry@npm:1.0.2" - dependencies: - retry: "npm:^0.13.1" - checksum: 10c0/748a84fb0ab962f7867966f21dc24d1872c53c1656dd3352320fe69ad3b2043f2dfdb3be024c7636ce4904c5ba1da22d0f3558e489c3de578f5bb520f062d0fd - languageName: node - linkType: hard - -"@isaacs/fs-minipass@npm:^4.0.0": - version: 4.0.1 - resolution: "@isaacs/fs-minipass@npm:4.0.1" - dependencies: - minipass: "npm:^7.0.4" - checksum: 10c0/c25b6dc1598790d5b55c0947a9b7d111cfa92594db5296c3b907e2f533c033666f692a3939eadac17b1c7c40d362d0b0635dc874cbfe3e70db7c2b07cc97a5d2 - languageName: node - linkType: hard - -"@jridgewell/gen-mapping@npm:^0.3.12, @jridgewell/gen-mapping@npm:^0.3.5": - version: 0.3.13 - resolution: "@jridgewell/gen-mapping@npm:0.3.13" - dependencies: - "@jridgewell/sourcemap-codec": "npm:^1.5.0" - "@jridgewell/trace-mapping": "npm:^0.3.24" - checksum: 10c0/9a7d65fb13bd9aec1fbab74cda08496839b7e2ceb31f5ab922b323e94d7c481ce0fc4fd7e12e2610915ed8af51178bdc61e168e92a8c8b8303b030b03489b13b - languageName: node - linkType: hard - -"@jridgewell/resolve-uri@npm:^3.1.0": - version: 3.1.2 - resolution: "@jridgewell/resolve-uri@npm:3.1.2" - checksum: 10c0/d502e6fb516b35032331406d4e962c21fe77cdf1cbdb49c6142bcbd9e30507094b18972778a6e27cbad756209cfe34b1a27729e6fa08a2eb92b33943f680cf1e - languageName: node - linkType: hard - -"@jridgewell/source-map@npm:^0.3.3": - version: 0.3.11 - resolution: "@jridgewell/source-map@npm:0.3.11" - dependencies: - "@jridgewell/gen-mapping": "npm:^0.3.5" - "@jridgewell/trace-mapping": "npm:^0.3.25" - checksum: 10c0/50a4fdafe0b8f655cb2877e59fe81320272eaa4ccdbe6b9b87f10614b2220399ae3e05c16137a59db1f189523b42c7f88bd097ee991dbd7bc0e01113c583e844 - languageName: node - linkType: hard - -"@jridgewell/sourcemap-codec@npm:^1.4.13, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.0": - version: 1.5.5 - resolution: "@jridgewell/sourcemap-codec@npm:1.5.5" - checksum: 10c0/f9e538f302b63c0ebc06eecb1dd9918dd4289ed36147a0ddce35d6ea4d7ebbda243cda7b2213b6a5e1d8087a298d5cf630fb2bd39329cdecb82017023f6081a0 - languageName: node - linkType: hard - -"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25, @jridgewell/trace-mapping@npm:^0.3.28": - version: 0.3.31 - resolution: "@jridgewell/trace-mapping@npm:0.3.31" - dependencies: - "@jridgewell/resolve-uri": "npm:^3.1.0" - "@jridgewell/sourcemap-codec": "npm:^1.4.14" - checksum: 10c0/4b30ec8cd56c5fd9a661f088230af01e0c1a3888d11ffb6b47639700f71225be21d1f7e168048d6d4f9449207b978a235c07c8f15c07705685d16dc06280e9d9 - languageName: node - linkType: hard - -"@ngtools/webpack@npm:15.2.11": - version: 15.2.11 - resolution: "@ngtools/webpack@npm:15.2.11" - peerDependencies: - "@angular/compiler-cli": ^15.0.0 - typescript: ">=4.8.2 <5.0" - webpack: ^5.54.0 - checksum: 10c0/2f3d4f87a10f399f0de459369e7d24d5543200cb96b61ef4c98a589d5cd323a6adca293118a9dc68312428ec8dbe49f5d46e08f65ce41e42f118e1636fb96367 - languageName: node - linkType: hard - -"@npmcli/agent@npm:^4.0.0": - version: 4.0.0 - resolution: "@npmcli/agent@npm:4.0.0" - dependencies: - agent-base: "npm:^7.1.0" - http-proxy-agent: "npm:^7.0.0" - https-proxy-agent: "npm:^7.0.1" - lru-cache: "npm:^11.2.1" - socks-proxy-agent: "npm:^8.0.3" - checksum: 10c0/f7b5ce0f3dd42c3f8c6546e8433573d8049f67ef11ec22aa4704bc41483122f68bf97752e06302c455ead667af5cb753e6a09bff06632bc465c1cfd4c4b75a53 - languageName: node - linkType: hard - -"@npmcli/fs@npm:^5.0.0": - version: 5.0.0 - resolution: "@npmcli/fs@npm:5.0.0" - dependencies: - semver: "npm:^7.3.5" - checksum: 10c0/26e376d780f60ff16e874a0ac9bc3399186846baae0b6e1352286385ac134d900cc5dafaded77f38d77f86898fc923ae1cee9d7399f0275b1aa24878915d722b - languageName: node - linkType: hard - -"@types/eslint-scope@npm:^3.7.7": - version: 3.7.7 - resolution: "@types/eslint-scope@npm:3.7.7" - dependencies: - "@types/eslint": "npm:*" - "@types/estree": "npm:*" - checksum: 10c0/a0ecbdf2f03912679440550817ff77ef39a30fa8bfdacaf6372b88b1f931828aec392f52283240f0d648cf3055c5ddc564544a626bcf245f3d09fcb099ebe3cc - languageName: node - linkType: hard - -"@types/eslint@npm:*": - version: 9.6.1 - resolution: "@types/eslint@npm:9.6.1" - dependencies: - "@types/estree": "npm:*" - "@types/json-schema": "npm:*" - checksum: 10c0/69ba24fee600d1e4c5abe0df086c1a4d798abf13792d8cfab912d76817fe1a894359a1518557d21237fbaf6eda93c5ab9309143dee4c59ef54336d1b3570420e - languageName: node - linkType: hard - -"@types/estree@npm:*, @types/estree@npm:^1.0.8": - version: 1.0.8 - resolution: "@types/estree@npm:1.0.8" - checksum: 10c0/39d34d1afaa338ab9763f37ad6066e3f349444f9052b9676a7cc0252ef9485a41c6d81c9c4e0d26e9077993354edf25efc853f3224dd4b447175ef62bdcc86a5 - languageName: node - linkType: hard - -"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.9": - version: 7.0.15 - resolution: "@types/json-schema@npm:7.0.15" - checksum: 10c0/a996a745e6c5d60292f36731dd41341339d4eeed8180bb09226e5c8d23759067692b1d88e5d91d72ee83dfc00d3aca8e7bd43ea120516c17922cbcb7c3e252db - languageName: node - linkType: hard - -"@types/node@npm:*": - version: 25.4.0 - resolution: "@types/node@npm:25.4.0" - dependencies: - undici-types: "npm:~7.18.0" - checksum: 10c0/da81e8b0a3a57964b1b5f85d134bfefc1b923fd67ed41756842348a049d7915b72e8773f5598d6929b9cb8119c2427993c55d364fd93bd572a3450e58b98a60e - languageName: node - linkType: hard - -"@webassemblyjs/ast@npm:1.14.1, @webassemblyjs/ast@npm:^1.14.1": - version: 1.14.1 - resolution: "@webassemblyjs/ast@npm:1.14.1" - dependencies: - "@webassemblyjs/helper-numbers": "npm:1.13.2" - "@webassemblyjs/helper-wasm-bytecode": "npm:1.13.2" - checksum: 10c0/67a59be8ed50ddd33fbb2e09daa5193ac215bf7f40a9371be9a0d9797a114d0d1196316d2f3943efdb923a3d809175e1563a3cb80c814fb8edccd1e77494972b - languageName: node - linkType: hard - -"@webassemblyjs/floating-point-hex-parser@npm:1.13.2": - version: 1.13.2 - resolution: "@webassemblyjs/floating-point-hex-parser@npm:1.13.2" - checksum: 10c0/0e88bdb8b50507d9938be64df0867f00396b55eba9df7d3546eb5dc0ca64d62e06f8d881ec4a6153f2127d0f4c11d102b6e7d17aec2f26bb5ff95a5e60652412 - languageName: node - linkType: hard - -"@webassemblyjs/helper-api-error@npm:1.13.2": - version: 1.13.2 - resolution: "@webassemblyjs/helper-api-error@npm:1.13.2" - checksum: 10c0/31be497f996ed30aae4c08cac3cce50c8dcd5b29660383c0155fce1753804fc55d47fcba74e10141c7dd2899033164e117b3bcfcda23a6b043e4ded4f1003dfb - languageName: node - linkType: hard - -"@webassemblyjs/helper-buffer@npm:1.14.1": - version: 1.14.1 - resolution: "@webassemblyjs/helper-buffer@npm:1.14.1" - checksum: 10c0/0d54105dc373c0fe6287f1091e41e3a02e36cdc05e8cf8533cdc16c59ff05a646355415893449d3768cda588af451c274f13263300a251dc11a575bc4c9bd210 - languageName: node - linkType: hard - -"@webassemblyjs/helper-numbers@npm:1.13.2": - version: 1.13.2 - resolution: "@webassemblyjs/helper-numbers@npm:1.13.2" - dependencies: - "@webassemblyjs/floating-point-hex-parser": "npm:1.13.2" - "@webassemblyjs/helper-api-error": "npm:1.13.2" - "@xtuc/long": "npm:4.2.2" - checksum: 10c0/9c46852f31b234a8fb5a5a9d3f027bc542392a0d4de32f1a9c0075d5e8684aa073cb5929b56df565500b3f9cc0a2ab983b650314295b9bf208d1a1651bfc825a - languageName: node - linkType: hard - -"@webassemblyjs/helper-wasm-bytecode@npm:1.13.2": - version: 1.13.2 - resolution: "@webassemblyjs/helper-wasm-bytecode@npm:1.13.2" - checksum: 10c0/c4355d14f369b30cf3cbdd3acfafc7d0488e086be6d578e3c9780bd1b512932352246be96e034e2a7fcfba4f540ec813352f312bfcbbfe5bcfbf694f82ccc682 - languageName: node - linkType: hard - -"@webassemblyjs/helper-wasm-section@npm:1.14.1": - version: 1.14.1 - resolution: "@webassemblyjs/helper-wasm-section@npm:1.14.1" - dependencies: - "@webassemblyjs/ast": "npm:1.14.1" - "@webassemblyjs/helper-buffer": "npm:1.14.1" - "@webassemblyjs/helper-wasm-bytecode": "npm:1.13.2" - "@webassemblyjs/wasm-gen": "npm:1.14.1" - checksum: 10c0/1f9b33731c3c6dbac3a9c483269562fa00d1b6a4e7133217f40e83e975e636fd0f8736e53abd9a47b06b66082ecc976c7384391ab0a68e12d509ea4e4b948d64 - languageName: node - linkType: hard - -"@webassemblyjs/ieee754@npm:1.13.2": - version: 1.13.2 - resolution: "@webassemblyjs/ieee754@npm:1.13.2" - dependencies: - "@xtuc/ieee754": "npm:^1.2.0" - checksum: 10c0/2e732ca78c6fbae3c9b112f4915d85caecdab285c0b337954b180460290ccd0fb00d2b1dc4bb69df3504abead5191e0d28d0d17dfd6c9d2f30acac8c4961c8a7 - languageName: node - linkType: hard - -"@webassemblyjs/leb128@npm:1.13.2": - version: 1.13.2 - resolution: "@webassemblyjs/leb128@npm:1.13.2" - dependencies: - "@xtuc/long": "npm:4.2.2" - checksum: 10c0/dad5ef9e383c8ab523ce432dfd80098384bf01c45f70eb179d594f85ce5db2f80fa8c9cba03adafd85684e6d6310f0d3969a882538975989919329ac4c984659 - languageName: node - linkType: hard - -"@webassemblyjs/utf8@npm:1.13.2": - version: 1.13.2 - resolution: "@webassemblyjs/utf8@npm:1.13.2" - checksum: 10c0/d3fac9130b0e3e5a1a7f2886124a278e9323827c87a2b971e6d0da22a2ba1278ac9f66a4f2e363ecd9fac8da42e6941b22df061a119e5c0335f81006de9ee799 - languageName: node - linkType: hard - -"@webassemblyjs/wasm-edit@npm:^1.14.1": - version: 1.14.1 - resolution: "@webassemblyjs/wasm-edit@npm:1.14.1" - dependencies: - "@webassemblyjs/ast": "npm:1.14.1" - "@webassemblyjs/helper-buffer": "npm:1.14.1" - "@webassemblyjs/helper-wasm-bytecode": "npm:1.13.2" - "@webassemblyjs/helper-wasm-section": "npm:1.14.1" - "@webassemblyjs/wasm-gen": "npm:1.14.1" - "@webassemblyjs/wasm-opt": "npm:1.14.1" - "@webassemblyjs/wasm-parser": "npm:1.14.1" - "@webassemblyjs/wast-printer": "npm:1.14.1" - checksum: 10c0/5ac4781086a2ca4b320bdbfd965a209655fe8a208ca38d89197148f8597e587c9a2c94fb6bd6f1a7dbd4527c49c6844fcdc2af981f8d793a97bf63a016aa86d2 - languageName: node - linkType: hard - -"@webassemblyjs/wasm-gen@npm:1.14.1": - version: 1.14.1 - resolution: "@webassemblyjs/wasm-gen@npm:1.14.1" - dependencies: - "@webassemblyjs/ast": "npm:1.14.1" - "@webassemblyjs/helper-wasm-bytecode": "npm:1.13.2" - "@webassemblyjs/ieee754": "npm:1.13.2" - "@webassemblyjs/leb128": "npm:1.13.2" - "@webassemblyjs/utf8": "npm:1.13.2" - checksum: 10c0/d678810d7f3f8fecb2e2bdadfb9afad2ec1d2bc79f59e4711ab49c81cec578371e22732d4966f59067abe5fba8e9c54923b57060a729d28d408e608beef67b10 - languageName: node - linkType: hard - -"@webassemblyjs/wasm-opt@npm:1.14.1": - version: 1.14.1 - resolution: "@webassemblyjs/wasm-opt@npm:1.14.1" - dependencies: - "@webassemblyjs/ast": "npm:1.14.1" - "@webassemblyjs/helper-buffer": "npm:1.14.1" - "@webassemblyjs/wasm-gen": "npm:1.14.1" - "@webassemblyjs/wasm-parser": "npm:1.14.1" - checksum: 10c0/515bfb15277ee99ba6b11d2232ddbf22aed32aad6d0956fe8a0a0a004a1b5a3a277a71d9a3a38365d0538ac40d1b7b7243b1a244ad6cd6dece1c1bb2eb5de7ee - languageName: node - linkType: hard - -"@webassemblyjs/wasm-parser@npm:1.14.1, @webassemblyjs/wasm-parser@npm:^1.14.1": - version: 1.14.1 - resolution: "@webassemblyjs/wasm-parser@npm:1.14.1" - dependencies: - "@webassemblyjs/ast": "npm:1.14.1" - "@webassemblyjs/helper-api-error": "npm:1.13.2" - "@webassemblyjs/helper-wasm-bytecode": "npm:1.13.2" - "@webassemblyjs/ieee754": "npm:1.13.2" - "@webassemblyjs/leb128": "npm:1.13.2" - "@webassemblyjs/utf8": "npm:1.13.2" - checksum: 10c0/95427b9e5addbd0f647939bd28e3e06b8deefdbdadcf892385b5edc70091bf9b92fa5faac3fce8333554437c5d85835afef8c8a7d9d27ab6ba01ffab954db8c6 - languageName: node - linkType: hard - -"@webassemblyjs/wast-printer@npm:1.14.1": - version: 1.14.1 - resolution: "@webassemblyjs/wast-printer@npm:1.14.1" - dependencies: - "@webassemblyjs/ast": "npm:1.14.1" - "@xtuc/long": "npm:4.2.2" - checksum: 10c0/8d7768608996a052545251e896eac079c98e0401842af8dd4de78fba8d90bd505efb6c537e909cd6dae96e09db3fa2e765a6f26492553a675da56e2db51f9d24 - languageName: node - linkType: hard - -"@webpack-cli/configtest@npm:^3.0.1": - version: 3.0.1 - resolution: "@webpack-cli/configtest@npm:3.0.1" - peerDependencies: - webpack: ^5.82.0 - webpack-cli: 6.x.x - checksum: 10c0/edd24ecfc429298fe86446f7d7daedfe82d72e7f6236c81420605484fdadade5d59c6bcef3d76bd724e11d9727f74e75de183223ae62d3a568b2d54199688cbe - languageName: node - linkType: hard - -"@webpack-cli/info@npm:^3.0.1": - version: 3.0.1 - resolution: "@webpack-cli/info@npm:3.0.1" - peerDependencies: - webpack: ^5.82.0 - webpack-cli: 6.x.x - checksum: 10c0/b23b94e7dc8c93e79248f20d5f1bd0fbb7b9ba4b012803e2fdc5440b8f2ee1f3eca7f4933bbca346c8168673bf572b1858169a3cb2c17d9b8bcd833d480c2170 - languageName: node - linkType: hard - -"@webpack-cli/serve@npm:^3.0.1": - version: 3.0.1 - resolution: "@webpack-cli/serve@npm:3.0.1" - peerDependencies: - webpack: ^5.82.0 - webpack-cli: 6.x.x - peerDependenciesMeta: - webpack-dev-server: - optional: true - checksum: 10c0/65245e45bfa35e11a5b30631b99cfed0c1b39b2cc8320fa2d2a4185264535618827d349ec032c58af4201d6236cbc43bec894fcb840fdd06314611537a80e210 - languageName: node - linkType: hard - -"@xtuc/ieee754@npm:^1.2.0": - version: 1.2.0 - resolution: "@xtuc/ieee754@npm:1.2.0" - checksum: 10c0/a8565d29d135039bd99ae4b2220d3e167d22cf53f867e491ed479b3f84f895742d0097f935b19aab90265a23d5d46711e4204f14c479ae3637fbf06c4666882f - languageName: node - linkType: hard - -"@xtuc/long@npm:4.2.2": - version: 4.2.2 - resolution: "@xtuc/long@npm:4.2.2" - checksum: 10c0/8582cbc69c79ad2d31568c412129bf23d2b1210a1dfb60c82d5a1df93334da4ee51f3057051658569e2c196d8dc33bc05ae6b974a711d0d16e801e1d0647ccd1 - languageName: node - linkType: hard - -"abbrev@npm:^4.0.0": - version: 4.0.0 - resolution: "abbrev@npm:4.0.0" - checksum: 10c0/b4cc16935235e80702fc90192e349e32f8ef0ed151ef506aa78c81a7c455ec18375c4125414b99f84b2e055199d66383e787675f0bcd87da7a4dbd59f9eac1d5 - languageName: node - linkType: hard - -"acorn-import-phases@npm:^1.0.3": - version: 1.0.4 - resolution: "acorn-import-phases@npm:1.0.4" - peerDependencies: - acorn: ^8.14.0 - checksum: 10c0/338eb46fc1aed5544f628344cb9af189450b401d152ceadbf1f5746901a5d923016cd0e7740d5606062d374fdf6941c29bb515d2bd133c4f4242d5d4cd73a3c7 - languageName: node - linkType: hard - -"acorn@npm:^8.15.0": - version: 8.16.0 - resolution: "acorn@npm:8.16.0" - bin: - acorn: bin/acorn - checksum: 10c0/c9c52697227661b68d0debaf972222d4f622aa06b185824164e153438afa7b08273432ca43ea792cadb24dada1d46f6f6bb1ef8de9956979288cc1b96bf9914e - languageName: node - linkType: hard - -"agent-base@npm:^7.1.0, agent-base@npm:^7.1.2": - version: 7.1.4 - resolution: "agent-base@npm:7.1.4" - checksum: 10c0/c2c9ab7599692d594b6a161559ada307b7a624fa4c7b03e3afdb5a5e31cd0e53269115b620fcab024c5ac6a6f37fa5eb2e004f076ad30f5f7e6b8b671f7b35fe - languageName: node - linkType: hard - -"ajv-formats@npm:^2.1.1": - version: 2.1.1 - resolution: "ajv-formats@npm:2.1.1" - dependencies: - ajv: "npm:^8.0.0" - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - checksum: 10c0/e43ba22e91b6a48d96224b83d260d3a3a561b42d391f8d3c6d2c1559f9aa5b253bfb306bc94bbeca1d967c014e15a6efe9a207309e95b3eaae07fcbcdc2af662 - languageName: node - linkType: hard - -"ajv-keywords@npm:^5.1.0": - version: 5.1.0 - resolution: "ajv-keywords@npm:5.1.0" - dependencies: - fast-deep-equal: "npm:^3.1.3" - peerDependencies: - ajv: ^8.8.2 - checksum: 10c0/18bec51f0171b83123ba1d8883c126e60c6f420cef885250898bf77a8d3e65e3bfb9e8564f497e30bdbe762a83e0d144a36931328616a973ee669dc74d4a9590 - languageName: node - linkType: hard - -"ajv@npm:^8.0.0, ajv@npm:^8.9.0": - version: 8.18.0 - resolution: "ajv@npm:8.18.0" - dependencies: - fast-deep-equal: "npm:^3.1.3" - fast-uri: "npm:^3.0.1" - json-schema-traverse: "npm:^1.0.0" - require-from-string: "npm:^2.0.2" - checksum: 10c0/e7517c426173513a07391be951879932bdf3348feaebd2199f5b901c20f99d60db8cd1591502d4d551dc82f594e82a05c4fe1c70139b15b8937f7afeaed9532f - languageName: node - linkType: hard - -"angular-app@workspace:.": - version: 0.0.0-use.local - resolution: "angular-app@workspace:." - dependencies: - "@angular/common": "npm:15.2.10" - "@angular/compiler": "npm:15.2.10" - "@angular/compiler-cli": "npm:15.2.10" - "@angular/core": "npm:15.2.10" - "@angular/platform-browser": "npm:15.2.10" - "@angular/router": "npm:15.2.10" - "@datadog/browser-rum": "file:../../../packages/rum/package.tgz" - "@datadog/browser-rum-angular": "file:../../../packages/rum-angular/package.tgz" - "@ngtools/webpack": "npm:15.2.11" - babel-loader: "npm:9.2.1" - rxjs: "npm:7.8.2" - typescript: "npm:4.9.5" - webpack: "npm:5.105.2" - webpack-cli: "npm:6.0.1" - zone.js: "npm:0.12.0" - languageName: unknown - linkType: soft - -"ansi-regex@npm:^5.0.1": - version: 5.0.1 - resolution: "ansi-regex@npm:5.0.1" - checksum: 10c0/9a64bb8627b434ba9327b60c027742e5d17ac69277960d041898596271d992d4d52ba7267a63ca10232e29f6107fc8a835f6ce8d719b88c5f8493f8254813737 - languageName: node - linkType: hard - -"ansi-styles@npm:^4.0.0": - version: 4.3.0 - resolution: "ansi-styles@npm:4.3.0" - dependencies: - color-convert: "npm:^2.0.1" - checksum: 10c0/895a23929da416f2bd3de7e9cb4eabd340949328ab85ddd6e484a637d8f6820d485f53933446f5291c3b760cbc488beb8e88573dd0f9c7daf83dccc8fe81b041 - languageName: node - linkType: hard - -"anymatch@npm:~3.1.2": - version: 3.1.3 - resolution: "anymatch@npm:3.1.3" - dependencies: - normalize-path: "npm:^3.0.0" - picomatch: "npm:^2.0.4" - checksum: 10c0/57b06ae984bc32a0d22592c87384cd88fe4511b1dd7581497831c56d41939c8a001b28e7b853e1450f2bf61992dfcaa8ae2d0d161a0a90c4fb631ef07098fbac - languageName: node - linkType: hard - -"babel-loader@npm:9.2.1": - version: 9.2.1 - resolution: "babel-loader@npm:9.2.1" - dependencies: - find-cache-dir: "npm:^4.0.0" - schema-utils: "npm:^4.0.0" - peerDependencies: - "@babel/core": ^7.12.0 - webpack: ">=5" - checksum: 10c0/efb82faff4c7c27e9c15bb28bf11c73200e61cf365118a9514e8d74dd489d0afc2a0d5aaa62cb4254eefc2ab631579224d95a03fd245410f28ea75e24de54ba4 - languageName: node - linkType: hard - -"balanced-match@npm:^4.0.2": - version: 4.0.4 - resolution: "balanced-match@npm:4.0.4" - checksum: 10c0/07e86102a3eb2ee2a6a1a89164f29d0dbaebd28f2ca3f5ca786f36b8b23d9e417eb3be45a4acf754f837be5ac0a2317de90d3fcb7f4f4dc95720a1f36b26a17b - languageName: node - linkType: hard - -"baseline-browser-mapping@npm:^2.9.0": - version: 2.10.0 - resolution: "baseline-browser-mapping@npm:2.10.0" - bin: - baseline-browser-mapping: dist/cli.cjs - checksum: 10c0/da9c3ec0fcd7f325226a47d2142794d41706b6e0a405718a2c15410bbdb72aacadd65738bedef558c6f1b106ed19458cb25b06f63b66df2c284799905dbbd003 - languageName: node - linkType: hard - -"binary-extensions@npm:^2.0.0": - version: 2.3.0 - resolution: "binary-extensions@npm:2.3.0" - checksum: 10c0/75a59cafc10fb12a11d510e77110c6c7ae3f4ca22463d52487709ca7f18f69d886aa387557cc9864fbdb10153d0bdb4caacabf11541f55e89ed6e18d12ece2b5 - languageName: node - linkType: hard - -"brace-expansion@npm:^5.0.2": - version: 5.0.4 - resolution: "brace-expansion@npm:5.0.4" - dependencies: - balanced-match: "npm:^4.0.2" - checksum: 10c0/359cbcfa80b2eb914ca1f3440e92313fbfe7919ee6b274c35db55bec555aded69dac5ee78f102cec90c35f98c20fa43d10936d0cd9978158823c249257e1643a - languageName: node - linkType: hard - -"braces@npm:~3.0.2": - version: 3.0.3 - resolution: "braces@npm:3.0.3" - dependencies: - fill-range: "npm:^7.1.1" - checksum: 10c0/7c6dfd30c338d2997ba77500539227b9d1f85e388a5f43220865201e407e076783d0881f2d297b9f80951b4c957fcf0b51c1d2d24227631643c3f7c284b0aa04 - languageName: node - linkType: hard - -"browserslist@npm:^4.24.0, browserslist@npm:^4.28.1": - version: 4.28.1 - resolution: "browserslist@npm:4.28.1" - dependencies: - baseline-browser-mapping: "npm:^2.9.0" - caniuse-lite: "npm:^1.0.30001759" - electron-to-chromium: "npm:^1.5.263" - node-releases: "npm:^2.0.27" - update-browserslist-db: "npm:^1.2.0" - bin: - browserslist: cli.js - checksum: 10c0/545a5fa9d7234e3777a7177ec1e9134bb2ba60a69e6b95683f6982b1473aad347c77c1264ccf2ac5dea609a9731fbfbda6b85782bdca70f80f86e28a402504bd - languageName: node - linkType: hard - -"buffer-from@npm:^1.0.0": - version: 1.1.2 - resolution: "buffer-from@npm:1.1.2" - checksum: 10c0/124fff9d66d691a86d3b062eff4663fe437a9d9ee4b47b1b9e97f5a5d14f6d5399345db80f796827be7c95e70a8e765dd404b7c3ff3b3324f98e9b0c8826cc34 - languageName: node - linkType: hard - -"cacache@npm:^20.0.1": - version: 20.0.3 - resolution: "cacache@npm:20.0.3" - dependencies: - "@npmcli/fs": "npm:^5.0.0" - fs-minipass: "npm:^3.0.0" - glob: "npm:^13.0.0" - lru-cache: "npm:^11.1.0" - minipass: "npm:^7.0.3" - minipass-collect: "npm:^2.0.1" - minipass-flush: "npm:^1.0.5" - minipass-pipeline: "npm:^1.2.4" - p-map: "npm:^7.0.2" - ssri: "npm:^13.0.0" - unique-filename: "npm:^5.0.0" - checksum: 10c0/c7da1ca694d20e8f8aedabd21dc11518f809a7d2b59aa76a1fc655db5a9e62379e465c157ddd2afe34b19230808882288effa6911b2de26a088a6d5645123462 - languageName: node - linkType: hard - -"caniuse-lite@npm:^1.0.30001759": - version: 1.0.30001777 - resolution: "caniuse-lite@npm:1.0.30001777" - checksum: 10c0/e35443fa7c470edc06e315297cca706790840e96983fff12dfe502a4b123d6e4a64b9b4e8e35fb2f5bb60c31b24fbda93d76b2f700ce183df474671236fa7a4a - languageName: node - linkType: hard - -"chokidar@npm:^3.0.0": - version: 3.6.0 - resolution: "chokidar@npm:3.6.0" - dependencies: - anymatch: "npm:~3.1.2" - braces: "npm:~3.0.2" - fsevents: "npm:~2.3.2" - glob-parent: "npm:~5.1.2" - is-binary-path: "npm:~2.1.0" - is-glob: "npm:~4.0.1" - normalize-path: "npm:~3.0.0" - readdirp: "npm:~3.6.0" - dependenciesMeta: - fsevents: - optional: true - checksum: 10c0/8361dcd013f2ddbe260eacb1f3cb2f2c6f2b0ad118708a343a5ed8158941a39cb8fb1d272e0f389712e74ee90ce8ba864eece9e0e62b9705cb468a2f6d917462 - languageName: node - linkType: hard - -"chownr@npm:^3.0.0": - version: 3.0.0 - resolution: "chownr@npm:3.0.0" - checksum: 10c0/43925b87700f7e3893296c8e9c56cc58f926411cce3a6e5898136daaf08f08b9a8eb76d37d3267e707d0dcc17aed2e2ebdf5848c0c3ce95cf910a919935c1b10 - languageName: node - linkType: hard - -"chrome-trace-event@npm:^1.0.2": - version: 1.0.4 - resolution: "chrome-trace-event@npm:1.0.4" - checksum: 10c0/3058da7a5f4934b87cf6a90ef5fb68ebc5f7d06f143ed5a4650208e5d7acae47bc03ec844b29fbf5ba7e46e8daa6acecc878f7983a4f4bb7271593da91e61ff5 - languageName: node - linkType: hard - -"cliui@npm:^8.0.1": - version: 8.0.1 - resolution: "cliui@npm:8.0.1" - dependencies: - string-width: "npm:^4.2.0" - strip-ansi: "npm:^6.0.1" - wrap-ansi: "npm:^7.0.0" - checksum: 10c0/4bda0f09c340cbb6dfdc1ed508b3ca080f12992c18d68c6be4d9cf51756033d5266e61ec57529e610dacbf4da1c634423b0c1b11037709cc6b09045cbd815df5 - languageName: node - linkType: hard - -"clone-deep@npm:^4.0.1": - version: 4.0.1 - resolution: "clone-deep@npm:4.0.1" - dependencies: - is-plain-object: "npm:^2.0.4" - kind-of: "npm:^6.0.2" - shallow-clone: "npm:^3.0.0" - checksum: 10c0/637753615aa24adf0f2d505947a1bb75e63964309034a1cf56ba4b1f30af155201edd38d26ffe26911adaae267a3c138b344a4947d39f5fc1b6d6108125aa758 - languageName: node - linkType: hard - -"color-convert@npm:^2.0.1": - version: 2.0.1 - resolution: "color-convert@npm:2.0.1" - dependencies: - color-name: "npm:~1.1.4" - checksum: 10c0/37e1150172f2e311fe1b2df62c6293a342ee7380da7b9cfdba67ea539909afbd74da27033208d01d6d5cfc65ee7868a22e18d7e7648e004425441c0f8a15a7d7 - languageName: node - linkType: hard - -"color-name@npm:~1.1.4": - version: 1.1.4 - resolution: "color-name@npm:1.1.4" - checksum: 10c0/a1a3f914156960902f46f7f56bc62effc6c94e84b2cae157a526b1c1f74b677a47ec602bf68a61abfa2b42d15b7c5651c6dbe72a43af720bc588dff885b10f95 - languageName: node - linkType: hard - -"colorette@npm:^2.0.14": - version: 2.0.20 - resolution: "colorette@npm:2.0.20" - checksum: 10c0/e94116ff33b0ff56f3b83b9ace895e5bf87c2a7a47b3401b8c3f3226e050d5ef76cf4072fb3325f9dc24d1698f9b730baf4e05eeaf861d74a1883073f4c98a40 - languageName: node - linkType: hard - -"commander@npm:^12.1.0": - version: 12.1.0 - resolution: "commander@npm:12.1.0" - checksum: 10c0/6e1996680c083b3b897bfc1cfe1c58dfbcd9842fd43e1aaf8a795fbc237f65efcc860a3ef457b318e73f29a4f4a28f6403c3d653d021d960e4632dd45bde54a9 - languageName: node - linkType: hard - -"commander@npm:^2.20.0": - version: 2.20.3 - resolution: "commander@npm:2.20.3" - checksum: 10c0/74c781a5248c2402a0a3e966a0a2bba3c054aad144f5c023364be83265e796b20565aa9feff624132ff629aa64e16999fa40a743c10c12f7c61e96a794b99288 - languageName: node - linkType: hard - -"common-path-prefix@npm:^3.0.0": - version: 3.0.0 - resolution: "common-path-prefix@npm:3.0.0" - checksum: 10c0/c4a74294e1b1570f4a8ab435285d185a03976c323caa16359053e749db4fde44e3e6586c29cd051100335e11895767cbbd27ea389108e327d62f38daf4548fdb - languageName: node - linkType: hard - -"convert-source-map@npm:^1.5.1, convert-source-map@npm:^1.7.0": - version: 1.9.0 - resolution: "convert-source-map@npm:1.9.0" - checksum: 10c0/281da55454bf8126cbc6625385928c43479f2060984180c42f3a86c8b8c12720a24eac260624a7d1e090004028d2dee78602330578ceec1a08e27cb8bb0a8a5b - languageName: node - linkType: hard - -"cross-spawn@npm:^7.0.3": - version: 7.0.6 - resolution: "cross-spawn@npm:7.0.6" - dependencies: - path-key: "npm:^3.1.0" - shebang-command: "npm:^2.0.0" - which: "npm:^2.0.1" - checksum: 10c0/053ea8b2135caff68a9e81470e845613e374e7309a47731e81639de3eaeb90c3d01af0e0b44d2ab9d50b43467223b88567dfeb3262db942dc063b9976718ffc1 - languageName: node - linkType: hard - -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.3.1, debug@npm:^4.3.4": - version: 4.4.3 - resolution: "debug@npm:4.4.3" - dependencies: - ms: "npm:^2.1.3" - peerDependenciesMeta: - supports-color: - optional: true - checksum: 10c0/d79136ec6c83ecbefd0f6a5593da6a9c91ec4d7ddc4b54c883d6e71ec9accb5f67a1a5e96d00a328196b5b5c86d365e98d8a3a70856aaf16b4e7b1985e67f5a6 - languageName: node - linkType: hard - -"dependency-graph@npm:^0.11.0": - version: 0.11.0 - resolution: "dependency-graph@npm:0.11.0" - checksum: 10c0/9e6968d1534fdb502f7f3a25a3819b499f9d60f8389193950ed0b4d1618f1341b36b5d039f2cee256cfe10c9e8198ace16b271e370df06a93fac206e81602e7c - languageName: node - linkType: hard - -"electron-to-chromium@npm:^1.5.263": - version: 1.5.307 - resolution: "electron-to-chromium@npm:1.5.307" - checksum: 10c0/eb773a28af0dd7b3717b9bc2b31f332bcb42b43019866e039276db75c8c14063f96e29d19bea47231b4335a319d8518997b2d577dec6b5b237b768c7afdc5588 - languageName: node - linkType: hard - -"emoji-regex@npm:^8.0.0": - version: 8.0.0 - resolution: "emoji-regex@npm:8.0.0" - checksum: 10c0/b6053ad39951c4cf338f9092d7bfba448cdfd46fe6a2a034700b149ac9ffbc137e361cbd3c442297f86bed2e5f7576c1b54cc0a6bf8ef5106cc62f496af35010 - languageName: node - linkType: hard - -"enhanced-resolve@npm:^5.19.0": - version: 5.20.0 - resolution: "enhanced-resolve@npm:5.20.0" - dependencies: - graceful-fs: "npm:^4.2.4" - tapable: "npm:^2.3.0" - checksum: 10c0/4ed5f38406fc9ad74c58a3d63b8215862243ab0ed6b0efc51ccdb72cdcedd3ac8638abe298680b279d7a83c3cb140e5eea7a5f8bd99696c74588f07ad89a95a7 - languageName: node - linkType: hard - -"env-paths@npm:^2.2.0": - version: 2.2.1 - resolution: "env-paths@npm:2.2.1" - checksum: 10c0/285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4 - languageName: node - linkType: hard - -"envinfo@npm:^7.14.0": - version: 7.21.0 - resolution: "envinfo@npm:7.21.0" - bin: - envinfo: dist/cli.js - checksum: 10c0/4170127ca72dbf85be2c114f85558bd08178e8a43b394951ba9fd72d067c6fea3374df45a7b040e39e4e7b30bdd268e5bdf8661d99ae28302c2a88dedb41b5e6 - languageName: node - linkType: hard - -"es-module-lexer@npm:^2.0.0": - version: 2.0.0 - resolution: "es-module-lexer@npm:2.0.0" - checksum: 10c0/ae78dbbd43035a4b972c46cfb6877e374ea290adfc62bc2f5a083fea242c0b2baaab25c5886af86be55f092f4a326741cb94334cd3c478c383fdc8a9ec5ff817 - languageName: node - linkType: hard - -"escalade@npm:^3.1.1, escalade@npm:^3.2.0": - version: 3.2.0 - resolution: "escalade@npm:3.2.0" - checksum: 10c0/ced4dd3a78e15897ed3be74e635110bbf3b08877b0a41be50dcb325ee0e0b5f65fc2d50e9845194d7c4633f327e2e1c6cce00a71b617c5673df0374201d67f65 - languageName: node - linkType: hard - -"eslint-scope@npm:5.1.1": - version: 5.1.1 - resolution: "eslint-scope@npm:5.1.1" - dependencies: - esrecurse: "npm:^4.3.0" - estraverse: "npm:^4.1.1" - checksum: 10c0/d30ef9dc1c1cbdece34db1539a4933fe3f9b14e1ffb27ecc85987902ee663ad7c9473bbd49a9a03195a373741e62e2f807c4938992e019b511993d163450e70a - languageName: node - linkType: hard - -"esrecurse@npm:^4.3.0": - version: 4.3.0 - resolution: "esrecurse@npm:4.3.0" - dependencies: - estraverse: "npm:^5.2.0" - checksum: 10c0/81a37116d1408ded88ada45b9fb16dbd26fba3aadc369ce50fcaf82a0bac12772ebd7b24cd7b91fc66786bf2c1ac7b5f196bc990a473efff972f5cb338877cf5 - languageName: node - linkType: hard - -"estraverse@npm:^4.1.1": - version: 4.3.0 - resolution: "estraverse@npm:4.3.0" - checksum: 10c0/9cb46463ef8a8a4905d3708a652d60122a0c20bb58dec7e0e12ab0e7235123d74214fc0141d743c381813e1b992767e2708194f6f6e0f9fd00c1b4e0887b8b6d - languageName: node - linkType: hard - -"estraverse@npm:^5.2.0": - version: 5.3.0 - resolution: "estraverse@npm:5.3.0" - checksum: 10c0/1ff9447b96263dec95d6d67431c5e0771eb9776427421260a3e2f0fdd5d6bd4f8e37a7338f5ad2880c9f143450c9b1e4fc2069060724570a49cf9cf0312bd107 - languageName: node - linkType: hard - -"events@npm:^3.2.0": - version: 3.3.0 - resolution: "events@npm:3.3.0" - checksum: 10c0/d6b6f2adbccbcda74ddbab52ed07db727ef52e31a61ed26db9feb7dc62af7fc8e060defa65e5f8af9449b86b52cc1a1f6a79f2eafcf4e62add2b7a1fa4a432f6 - languageName: node - linkType: hard - -"exponential-backoff@npm:^3.1.1": - version: 3.1.3 - resolution: "exponential-backoff@npm:3.1.3" - checksum: 10c0/77e3ae682b7b1f4972f563c6dbcd2b0d54ac679e62d5d32f3e5085feba20483cf28bd505543f520e287a56d4d55a28d7874299941faf637e779a1aa5994d1267 - languageName: node - linkType: hard - -"fast-deep-equal@npm:^3.1.3": - version: 3.1.3 - resolution: "fast-deep-equal@npm:3.1.3" - checksum: 10c0/40dedc862eb8992c54579c66d914635afbec43350afbbe991235fdcb4e3a8d5af1b23ae7e79bef7d4882d0ecee06c3197488026998fb19f72dc95acff1d1b1d0 - languageName: node - linkType: hard - -"fast-uri@npm:^3.0.1": - version: 3.1.0 - resolution: "fast-uri@npm:3.1.0" - checksum: 10c0/44364adca566f70f40d1e9b772c923138d47efeac2ae9732a872baafd77061f26b097ba2f68f0892885ad177becd065520412b8ffeec34b16c99433c5b9e2de7 - languageName: node - linkType: hard - -"fastest-levenshtein@npm:^1.0.12": - version: 1.0.16 - resolution: "fastest-levenshtein@npm:1.0.16" - checksum: 10c0/7e3d8ae812a7f4fdf8cad18e9cde436a39addf266a5986f653ea0d81e0de0900f50c0f27c6d5aff3f686bcb48acbd45be115ae2216f36a6a13a7dbbf5cad878b - languageName: node - linkType: hard - -"fdir@npm:^6.5.0": - version: 6.5.0 - resolution: "fdir@npm:6.5.0" - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - checksum: 10c0/e345083c4306b3aed6cb8ec551e26c36bab5c511e99ea4576a16750ddc8d3240e63826cc624f5ae17ad4dc82e68a253213b60d556c11bfad064b7607847ed07f - languageName: node - linkType: hard - -"fill-range@npm:^7.1.1": - version: 7.1.1 - resolution: "fill-range@npm:7.1.1" - dependencies: - to-regex-range: "npm:^5.0.1" - checksum: 10c0/b75b691bbe065472f38824f694c2f7449d7f5004aa950426a2c28f0306c60db9b880c0b0e4ed819997ffb882d1da02cfcfc819bddc94d71627f5269682edf018 - languageName: node - linkType: hard - -"find-cache-dir@npm:^4.0.0": - version: 4.0.0 - resolution: "find-cache-dir@npm:4.0.0" - dependencies: - common-path-prefix: "npm:^3.0.0" - pkg-dir: "npm:^7.0.0" - checksum: 10c0/0faa7956974726c8769671de696d24c643ca1e5b8f7a2401283caa9e07a5da093293e0a0f4bd18c920ec981d2ef945c7f5b946cde268dfc9077d833ad0293cff - languageName: node - linkType: hard - -"find-up@npm:^4.0.0": - version: 4.1.0 - resolution: "find-up@npm:4.1.0" - dependencies: - locate-path: "npm:^5.0.0" - path-exists: "npm:^4.0.0" - checksum: 10c0/0406ee89ebeefa2d507feb07ec366bebd8a6167ae74aa4e34fb4c4abd06cf782a3ce26ae4194d70706f72182841733f00551c209fe575cb00bd92104056e78c1 - languageName: node - linkType: hard - -"find-up@npm:^6.3.0": - version: 6.3.0 - resolution: "find-up@npm:6.3.0" - dependencies: - locate-path: "npm:^7.1.0" - path-exists: "npm:^5.0.0" - checksum: 10c0/07e0314362d316b2b13f7f11ea4692d5191e718ca3f7264110127520f3347996349bf9e16805abae3e196805814bc66ef4bff2b8904dc4a6476085fc9b0eba07 - languageName: node - linkType: hard - -"flat@npm:^5.0.2": - version: 5.0.2 - resolution: "flat@npm:5.0.2" - bin: - flat: cli.js - checksum: 10c0/f178b13482f0cd80c7fede05f4d10585b1f2fdebf26e12edc138e32d3150c6ea6482b7f12813a1091143bad52bb6d3596bca51a162257a21163c0ff438baa5fe - languageName: node - linkType: hard - -"fs-minipass@npm:^3.0.0": - version: 3.0.3 - resolution: "fs-minipass@npm:3.0.3" - dependencies: - minipass: "npm:^7.0.3" - checksum: 10c0/63e80da2ff9b621e2cb1596abcb9207f1cf82b968b116ccd7b959e3323144cce7fb141462200971c38bbf2ecca51695069db45265705bed09a7cd93ae5b89f94 - languageName: node - linkType: hard - -"fsevents@npm:~2.3.2": - version: 2.3.3 - resolution: "fsevents@npm:2.3.3" - dependencies: - node-gyp: "npm:latest" - checksum: 10c0/a1f0c44595123ed717febbc478aa952e47adfc28e2092be66b8ab1635147254ca6cfe1df792a8997f22716d4cbafc73309899ff7bfac2ac3ad8cf2e4ecc3ec60 - conditions: os=darwin - languageName: node - linkType: hard - -"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin": - version: 2.3.3 - resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" - dependencies: - node-gyp: "npm:latest" - conditions: os=darwin - languageName: node - linkType: hard - -"function-bind@npm:^1.1.2": - version: 1.1.2 - resolution: "function-bind@npm:1.1.2" - checksum: 10c0/d8680ee1e5fcd4c197e4ac33b2b4dce03c71f4d91717292785703db200f5c21f977c568d28061226f9b5900cbcd2c84463646134fd5337e7925e0942bc3f46d5 - languageName: node - linkType: hard - -"gensync@npm:^1.0.0-beta.2": - version: 1.0.0-beta.2 - resolution: "gensync@npm:1.0.0-beta.2" - checksum: 10c0/782aba6cba65b1bb5af3b095d96249d20edbe8df32dbf4696fd49be2583faf676173bf4809386588828e4dd76a3354fcbeb577bab1c833ccd9fc4577f26103f8 - languageName: node - linkType: hard - -"get-caller-file@npm:^2.0.5": - version: 2.0.5 - resolution: "get-caller-file@npm:2.0.5" - checksum: 10c0/c6c7b60271931fa752aeb92f2b47e355eac1af3a2673f47c9589e8f8a41adc74d45551c1bc57b5e66a80609f10ffb72b6f575e4370d61cc3f7f3aaff01757cde - languageName: node - linkType: hard - -"glob-parent@npm:~5.1.2": - version: 5.1.2 - resolution: "glob-parent@npm:5.1.2" - dependencies: - is-glob: "npm:^4.0.1" - checksum: 10c0/cab87638e2112bee3f839ef5f6e0765057163d39c66be8ec1602f3823da4692297ad4e972de876ea17c44d652978638d2fd583c6713d0eb6591706825020c9ee - languageName: node - linkType: hard - -"glob-to-regexp@npm:^0.4.1": - version: 0.4.1 - resolution: "glob-to-regexp@npm:0.4.1" - checksum: 10c0/0486925072d7a916f052842772b61c3e86247f0a80cc0deb9b5a3e8a1a9faad5b04fb6f58986a09f34d3e96cd2a22a24b7e9882fb1cf904c31e9a310de96c429 - languageName: node - linkType: hard - -"glob@npm:^13.0.0": - version: 13.0.6 - resolution: "glob@npm:13.0.6" - dependencies: - minimatch: "npm:^10.2.2" - minipass: "npm:^7.1.3" - path-scurry: "npm:^2.0.2" - checksum: 10c0/269c236f11a9b50357fe7a8c6aadac667e01deb5242b19c84975628f05f4438d8ee1354bb62c5d6c10f37fd59911b54d7799730633a2786660d8c69f1d18120a - languageName: node - linkType: hard - -"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6": - version: 4.2.11 - resolution: "graceful-fs@npm:4.2.11" - checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 - languageName: node - linkType: hard - -"has-flag@npm:^4.0.0": - version: 4.0.0 - resolution: "has-flag@npm:4.0.0" - checksum: 10c0/2e789c61b7888d66993e14e8331449e525ef42aac53c627cc53d1c3334e768bcb6abdc4f5f0de1478a25beec6f0bd62c7549058b7ac53e924040d4f301f02fd1 - languageName: node - linkType: hard - -"hasown@npm:^2.0.2": - version: 2.0.2 - resolution: "hasown@npm:2.0.2" - dependencies: - function-bind: "npm:^1.1.2" - checksum: 10c0/3769d434703b8ac66b209a4cca0737519925bbdb61dd887f93a16372b14694c63ff4e797686d87c90f08168e81082248b9b028bad60d4da9e0d1148766f56eb9 - languageName: node - linkType: hard - -"http-cache-semantics@npm:^4.1.1": - version: 4.2.0 - resolution: "http-cache-semantics@npm:4.2.0" - checksum: 10c0/45b66a945cf13ec2d1f29432277201313babf4a01d9e52f44b31ca923434083afeca03f18417f599c9ab3d0e7b618ceb21257542338b57c54b710463b4a53e37 - languageName: node - linkType: hard - -"http-proxy-agent@npm:^7.0.0": - version: 7.0.2 - resolution: "http-proxy-agent@npm:7.0.2" - dependencies: - agent-base: "npm:^7.1.0" - debug: "npm:^4.3.4" - checksum: 10c0/4207b06a4580fb85dd6dff521f0abf6db517489e70863dca1a0291daa7f2d3d2d6015a57bd702af068ea5cf9f1f6ff72314f5f5b4228d299c0904135d2aef921 - languageName: node - linkType: hard - -"https-proxy-agent@npm:^7.0.1": - version: 7.0.6 - resolution: "https-proxy-agent@npm:7.0.6" - dependencies: - agent-base: "npm:^7.1.2" - debug: "npm:4" - checksum: 10c0/f729219bc735edb621fa30e6e84e60ee5d00802b8247aac0d7b79b0bd6d4b3294737a337b93b86a0bd9e68099d031858a39260c976dc14cdbba238ba1f8779ac - languageName: node - linkType: hard - -"iconv-lite@npm:^0.7.2": - version: 0.7.2 - resolution: "iconv-lite@npm:0.7.2" - dependencies: - safer-buffer: "npm:>= 2.1.2 < 3.0.0" - checksum: 10c0/3c228920f3bd307f56bf8363706a776f4a060eb042f131cd23855ceca962951b264d0997ab38a1ad340e1c5df8499ed26e1f4f0db6b2a2ad9befaff22f14b722 - languageName: node - linkType: hard - -"import-local@npm:^3.0.2": - version: 3.2.0 - resolution: "import-local@npm:3.2.0" - dependencies: - pkg-dir: "npm:^4.2.0" - resolve-cwd: "npm:^3.0.0" - bin: - import-local-fixture: fixtures/cli.js - checksum: 10c0/94cd6367a672b7e0cb026970c85b76902d2710a64896fa6de93bd5c571dd03b228c5759308959de205083e3b1c61e799f019c9e36ee8e9c523b993e1057f0433 - languageName: node - linkType: hard - -"imurmurhash@npm:^0.1.4": - version: 0.1.4 - resolution: "imurmurhash@npm:0.1.4" - checksum: 10c0/8b51313850dd33605c6c9d3fd9638b714f4c4c40250cff658209f30d40da60f78992fb2df5dabee4acf589a6a82bbc79ad5486550754bd9ec4e3fc0d4a57d6a6 - languageName: node - linkType: hard - -"interpret@npm:^3.1.1": - version: 3.1.1 - resolution: "interpret@npm:3.1.1" - checksum: 10c0/6f3c4d0aa6ec1b43a8862375588a249e3c917739895cbe67fe12f0a76260ea632af51e8e2431b50fbcd0145356dc28ca147be08dbe6a523739fd55c0f91dc2a5 - languageName: node - linkType: hard - -"ip-address@npm:^10.0.1": - version: 10.1.0 - resolution: "ip-address@npm:10.1.0" - checksum: 10c0/0103516cfa93f6433b3bd7333fa876eb21263912329bfa47010af5e16934eeeff86f3d2ae700a3744a137839ddfad62b900c7a445607884a49b5d1e32a3d7566 - languageName: node - linkType: hard - -"is-binary-path@npm:~2.1.0": - version: 2.1.0 - resolution: "is-binary-path@npm:2.1.0" - dependencies: - binary-extensions: "npm:^2.0.0" - checksum: 10c0/a16eaee59ae2b315ba36fad5c5dcaf8e49c3e27318f8ab8fa3cdb8772bf559c8d1ba750a589c2ccb096113bb64497084361a25960899cb6172a6925ab6123d38 - languageName: node - linkType: hard - -"is-core-module@npm:^2.16.1": - version: 2.16.1 - resolution: "is-core-module@npm:2.16.1" - dependencies: - hasown: "npm:^2.0.2" - checksum: 10c0/898443c14780a577e807618aaae2b6f745c8538eca5c7bc11388a3f2dc6de82b9902bcc7eb74f07be672b11bbe82dd6a6edded44a00cb3d8f933d0459905eedd - languageName: node - linkType: hard - -"is-extglob@npm:^2.1.1": - version: 2.1.1 - resolution: "is-extglob@npm:2.1.1" - checksum: 10c0/5487da35691fbc339700bbb2730430b07777a3c21b9ebaecb3072512dfd7b4ba78ac2381a87e8d78d20ea08affb3f1971b4af629173a6bf435ff8a4c47747912 - languageName: node - linkType: hard - -"is-fullwidth-code-point@npm:^3.0.0": - version: 3.0.0 - resolution: "is-fullwidth-code-point@npm:3.0.0" - checksum: 10c0/bb11d825e049f38e04c06373a8d72782eee0205bda9d908cc550ccb3c59b99d750ff9537982e01733c1c94a58e35400661f57042158ff5e8f3e90cf936daf0fc - languageName: node - linkType: hard - -"is-glob@npm:^4.0.1, is-glob@npm:~4.0.1": - version: 4.0.3 - resolution: "is-glob@npm:4.0.3" - dependencies: - is-extglob: "npm:^2.1.1" - checksum: 10c0/17fb4014e22be3bbecea9b2e3a76e9e34ff645466be702f1693e8f1ee1adac84710d0be0bd9f967d6354036fd51ab7c2741d954d6e91dae6bb69714de92c197a - languageName: node - linkType: hard - -"is-number@npm:^7.0.0": - version: 7.0.0 - resolution: "is-number@npm:7.0.0" - checksum: 10c0/b4686d0d3053146095ccd45346461bc8e53b80aeb7671cc52a4de02dbbf7dc0d1d2a986e2fe4ae206984b4d34ef37e8b795ebc4f4295c978373e6575e295d811 - languageName: node - linkType: hard - -"is-plain-object@npm:^2.0.4": - version: 2.0.4 - resolution: "is-plain-object@npm:2.0.4" - dependencies: - isobject: "npm:^3.0.1" - checksum: 10c0/f050fdd5203d9c81e8c4df1b3ff461c4bc64e8b5ca383bcdde46131361d0a678e80bcf00b5257646f6c636197629644d53bd8e2375aea633de09a82d57e942f4 - languageName: node - linkType: hard - -"isexe@npm:^2.0.0": - version: 2.0.0 - resolution: "isexe@npm:2.0.0" - checksum: 10c0/228cfa503fadc2c31596ab06ed6aa82c9976eec2bfd83397e7eaf06d0ccf42cd1dfd6743bf9aeb01aebd4156d009994c5f76ea898d2832c1fe342da923ca457d - languageName: node - linkType: hard - -"isexe@npm:^4.0.0": - version: 4.0.0 - resolution: "isexe@npm:4.0.0" - checksum: 10c0/5884815115bceac452877659a9c7726382531592f43dc29e5d48b7c4100661aed54018cb90bd36cb2eaeba521092570769167acbb95c18d39afdccbcca06c5ce - languageName: node - linkType: hard - -"isobject@npm:^3.0.1": - version: 3.0.1 - resolution: "isobject@npm:3.0.1" - checksum: 10c0/03344f5064a82f099a0cd1a8a407f4c0d20b7b8485e8e816c39f249e9416b06c322e8dec5b842b6bb8a06de0af9cb48e7bc1b5352f0fadc2f0abac033db3d4db - languageName: node - linkType: hard - -"jest-worker@npm:^27.4.5": - version: 27.5.1 - resolution: "jest-worker@npm:27.5.1" - dependencies: - "@types/node": "npm:*" - merge-stream: "npm:^2.0.0" - supports-color: "npm:^8.0.0" - checksum: 10c0/8c4737ffd03887b3c6768e4cc3ca0269c0336c1e4b1b120943958ddb035ed2a0fc6acab6dc99631720a3720af4e708ff84fb45382ad1e83c27946adf3623969b - languageName: node - linkType: hard - -"js-tokens@npm:^4.0.0": - version: 4.0.0 - resolution: "js-tokens@npm:4.0.0" - checksum: 10c0/e248708d377aa058eacf2037b07ded847790e6de892bbad3dac0abba2e759cb9f121b00099a65195616badcb6eca8d14d975cb3e89eb1cfda644756402c8aeed - languageName: node - linkType: hard - -"jsesc@npm:^3.0.2": - version: 3.1.0 - resolution: "jsesc@npm:3.1.0" - bin: - jsesc: bin/jsesc - checksum: 10c0/531779df5ec94f47e462da26b4cbf05eb88a83d9f08aac2ba04206508fc598527a153d08bd462bae82fc78b3eaa1a908e1a4a79f886e9238641c4cdefaf118b1 - languageName: node - linkType: hard - -"json-parse-even-better-errors@npm:^2.3.1": - version: 2.3.1 - resolution: "json-parse-even-better-errors@npm:2.3.1" - checksum: 10c0/140932564c8f0b88455432e0f33c4cb4086b8868e37524e07e723f4eaedb9425bdc2bafd71bd1d9765bd15fd1e2d126972bc83990f55c467168c228c24d665f3 - languageName: node - linkType: hard - -"json-schema-traverse@npm:^1.0.0": - version: 1.0.0 - resolution: "json-schema-traverse@npm:1.0.0" - checksum: 10c0/71e30015d7f3d6dc1c316d6298047c8ef98a06d31ad064919976583eb61e1018a60a0067338f0f79cabc00d84af3fcc489bd48ce8a46ea165d9541ba17fb30c6 - languageName: node - linkType: hard - -"json5@npm:^2.2.1": - version: 2.2.3 - resolution: "json5@npm:2.2.3" - bin: - json5: lib/cli.js - checksum: 10c0/5a04eed94810fa55c5ea138b2f7a5c12b97c3750bc63d11e511dcecbfef758003861522a070c2272764ee0f4e3e323862f386945aeb5b85b87ee43f084ba586c - languageName: node - linkType: hard - -"kind-of@npm:^6.0.2": - version: 6.0.3 - resolution: "kind-of@npm:6.0.3" - checksum: 10c0/61cdff9623dabf3568b6445e93e31376bee1cdb93f8ba7033d86022c2a9b1791a1d9510e026e6465ebd701a6dd2f7b0808483ad8838341ac52f003f512e0b4c4 - languageName: node - linkType: hard - -"loader-runner@npm:^4.3.1": - version: 4.3.1 - resolution: "loader-runner@npm:4.3.1" - checksum: 10c0/a523b6329f114e0a98317158e30a7dfce044b731521be5399464010472a93a15ece44757d1eaed1d8845019869c5390218bc1c7c3110f4eeaef5157394486eac - languageName: node - linkType: hard - -"locate-path@npm:^5.0.0": - version: 5.0.0 - resolution: "locate-path@npm:5.0.0" - dependencies: - p-locate: "npm:^4.1.0" - checksum: 10c0/33a1c5247e87e022f9713e6213a744557a3e9ec32c5d0b5efb10aa3a38177615bf90221a5592674857039c1a0fd2063b82f285702d37b792d973e9e72ace6c59 - languageName: node - linkType: hard - -"locate-path@npm:^7.1.0": - version: 7.2.0 - resolution: "locate-path@npm:7.2.0" - dependencies: - p-locate: "npm:^6.0.0" - checksum: 10c0/139e8a7fe11cfbd7f20db03923cacfa5db9e14fa14887ea121345597472b4a63c1a42a8a5187defeeff6acf98fd568da7382aa39682d38f0af27433953a97751 - languageName: node - linkType: hard - -"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1": - version: 11.2.6 - resolution: "lru-cache@npm:11.2.6" - checksum: 10c0/73bbffb298760e71b2bfe8ebc16a311c6a60ceddbba919cfedfd8635c2d125fbfb5a39b71818200e67973b11f8d59c5a9e31d6f90722e340e90393663a66e5cd - languageName: node - linkType: hard - -"lru-cache@npm:^5.1.1": - version: 5.1.1 - resolution: "lru-cache@npm:5.1.1" - dependencies: - yallist: "npm:^3.0.2" - checksum: 10c0/89b2ef2ef45f543011e38737b8a8622a2f8998cddf0e5437174ef8f1f70a8b9d14a918ab3e232cb3ba343b7abddffa667f0b59075b2b80e6b4d63c3de6127482 - languageName: node - linkType: hard - -"magic-string@npm:^0.27.0": - version: 0.27.0 - resolution: "magic-string@npm:0.27.0" - dependencies: - "@jridgewell/sourcemap-codec": "npm:^1.4.13" - checksum: 10c0/cddacfea14441ca57ae8a307bc3cf90bac69efaa4138dd9a80804cffc2759bf06f32da3a293fb13eaa96334b7d45b7768a34f1d226afae25d2f05b05a3bb37d8 - languageName: node - linkType: hard - -"make-fetch-happen@npm:^15.0.0": - version: 15.0.4 - resolution: "make-fetch-happen@npm:15.0.4" - dependencies: - "@gar/promise-retry": "npm:^1.0.0" - "@npmcli/agent": "npm:^4.0.0" - cacache: "npm:^20.0.1" - http-cache-semantics: "npm:^4.1.1" - minipass: "npm:^7.0.2" - minipass-fetch: "npm:^5.0.0" - minipass-flush: "npm:^1.0.5" - minipass-pipeline: "npm:^1.2.4" - negotiator: "npm:^1.0.0" - proc-log: "npm:^6.0.0" - ssri: "npm:^13.0.0" - checksum: 10c0/b874bf6879fc0b8ef3a3cafdddadea4d956acf94790f8dede1a9d3c74c7886b6cd3eb992616b8e5935e6fd550016a465f10ba51bf6723a0c6f4d98883ae2926b - languageName: node - linkType: hard - -"merge-stream@npm:^2.0.0": - version: 2.0.0 - resolution: "merge-stream@npm:2.0.0" - checksum: 10c0/867fdbb30a6d58b011449b8885601ec1690c3e41c759ecd5a9d609094f7aed0096c37823ff4a7190ef0b8f22cc86beb7049196ff68c016e3b3c671d0dac91ce5 - languageName: node - linkType: hard - -"mime-db@npm:1.52.0": - version: 1.52.0 - resolution: "mime-db@npm:1.52.0" - checksum: 10c0/0557a01deebf45ac5f5777fe7740b2a5c309c6d62d40ceab4e23da9f821899ce7a900b7ac8157d4548ddbb7beffe9abc621250e6d182b0397ec7f10c7b91a5aa - languageName: node - linkType: hard - -"mime-types@npm:^2.1.27": - version: 2.1.35 - resolution: "mime-types@npm:2.1.35" - dependencies: - mime-db: "npm:1.52.0" - checksum: 10c0/82fb07ec56d8ff1fc999a84f2f217aa46cb6ed1033fefaabd5785b9a974ed225c90dc72fff460259e66b95b73648596dbcc50d51ed69cdf464af2d237d3149b2 - languageName: node - linkType: hard - -"minimatch@npm:^10.2.2": - version: 10.2.4 - resolution: "minimatch@npm:10.2.4" - dependencies: - brace-expansion: "npm:^5.0.2" - checksum: 10c0/35f3dfb7b99b51efd46afd378486889f590e7efb10e0f6a10ba6800428cf65c9a8dedb74427d0570b318d749b543dc4e85f06d46d2858bc8cac7e1eb49a95945 - languageName: node - linkType: hard - -"minipass-collect@npm:^2.0.1": - version: 2.0.1 - resolution: "minipass-collect@npm:2.0.1" - dependencies: - minipass: "npm:^7.0.3" - checksum: 10c0/5167e73f62bb74cc5019594709c77e6a742051a647fe9499abf03c71dca75515b7959d67a764bdc4f8b361cf897fbf25e2d9869ee039203ed45240f48b9aa06e - languageName: node - linkType: hard - -"minipass-fetch@npm:^5.0.0": - version: 5.0.2 - resolution: "minipass-fetch@npm:5.0.2" - dependencies: - iconv-lite: "npm:^0.7.2" - minipass: "npm:^7.0.3" - minipass-sized: "npm:^2.0.0" - minizlib: "npm:^3.0.1" - dependenciesMeta: - iconv-lite: - optional: true - checksum: 10c0/ce4ab9f21cfabaead2097d95dd33f485af8072fbc6b19611bce694965393453a1639d641c2bcf1c48f2ea7d41ea7fab8278373f1d0bee4e63b0a5b2cdd0ef649 - languageName: node - linkType: hard - -"minipass-flush@npm:^1.0.5": - version: 1.0.5 - resolution: "minipass-flush@npm:1.0.5" - dependencies: - minipass: "npm:^3.0.0" - checksum: 10c0/2a51b63feb799d2bb34669205eee7c0eaf9dce01883261a5b77410c9408aa447e478efd191b4de6fc1101e796ff5892f8443ef20d9544385819093dbb32d36bd - languageName: node - linkType: hard - -"minipass-pipeline@npm:^1.2.4": - version: 1.2.4 - resolution: "minipass-pipeline@npm:1.2.4" - dependencies: - minipass: "npm:^3.0.0" - checksum: 10c0/cbda57cea20b140b797505dc2cac71581a70b3247b84480c1fed5ca5ba46c25ecc25f68bfc9e6dcb1a6e9017dab5c7ada5eab73ad4f0a49d84e35093e0c643f2 - languageName: node - linkType: hard - -"minipass-sized@npm:^2.0.0": - version: 2.0.0 - resolution: "minipass-sized@npm:2.0.0" - dependencies: - minipass: "npm:^7.1.2" - checksum: 10c0/f9201696a6f6d68610d04c9c83e3d2e5cb9c026aae1c8cbf7e17f386105cb79c1bb088dbc21bf0b1eb4f3fb5df384fd1e7aa3bf1f33868c416ae8c8a92679db8 - languageName: node - linkType: hard - -"minipass@npm:^3.0.0": - version: 3.3.6 - resolution: "minipass@npm:3.3.6" - dependencies: - yallist: "npm:^4.0.0" - checksum: 10c0/a114746943afa1dbbca8249e706d1d38b85ed1298b530f5808ce51f8e9e941962e2a5ad2e00eae7dd21d8a4aae6586a66d4216d1a259385e9d0358f0c1eba16c - languageName: node - linkType: hard - -"minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2, minipass@npm:^7.1.3": - version: 7.1.3 - resolution: "minipass@npm:7.1.3" - checksum: 10c0/539da88daca16533211ea5a9ee98dc62ff5742f531f54640dd34429e621955e91cc280a91a776026264b7f9f6735947629f920944e9c1558369e8bf22eb33fbb - languageName: node - linkType: hard - -"minizlib@npm:^3.0.1, minizlib@npm:^3.1.0": - version: 3.1.0 - resolution: "minizlib@npm:3.1.0" - dependencies: - minipass: "npm:^7.1.2" - checksum: 10c0/5aad75ab0090b8266069c9aabe582c021ae53eb33c6c691054a13a45db3b4f91a7fb1bd79151e6b4e9e9a86727b522527c0a06ec7d45206b745d54cd3097bcec - languageName: node - linkType: hard - -"ms@npm:^2.1.3": - version: 2.1.3 - resolution: "ms@npm:2.1.3" - checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 - languageName: node - linkType: hard - -"negotiator@npm:^1.0.0": - version: 1.0.0 - resolution: "negotiator@npm:1.0.0" - checksum: 10c0/4c559dd52669ea48e1914f9d634227c561221dd54734070791f999c52ed0ff36e437b2e07d5c1f6e32909fc625fe46491c16e4a8f0572567d4dd15c3a4fda04b - languageName: node - linkType: hard - -"neo-async@npm:^2.6.2": - version: 2.6.2 - resolution: "neo-async@npm:2.6.2" - checksum: 10c0/c2f5a604a54a8ec5438a342e1f356dff4bc33ccccdb6dc668d94fe8e5eccfc9d2c2eea6064b0967a767ba63b33763f51ccf2cd2441b461a7322656c1f06b3f5d - languageName: node - linkType: hard - -"node-gyp@npm:latest": - version: 12.2.0 - resolution: "node-gyp@npm:12.2.0" - dependencies: - env-paths: "npm:^2.2.0" - exponential-backoff: "npm:^3.1.1" - graceful-fs: "npm:^4.2.6" - make-fetch-happen: "npm:^15.0.0" - nopt: "npm:^9.0.0" - proc-log: "npm:^6.0.0" - semver: "npm:^7.3.5" - tar: "npm:^7.5.4" - tinyglobby: "npm:^0.2.12" - which: "npm:^6.0.0" - bin: - node-gyp: bin/node-gyp.js - checksum: 10c0/3ed046746a5a7d90950cd8b0547332b06598443f31fe213ef4332a7174c7b7d259e1704835feda79b87d3f02e59d7791842aac60642ede4396ab25fdf0f8f759 - languageName: node - linkType: hard - -"node-releases@npm:^2.0.27": - version: 2.0.36 - resolution: "node-releases@npm:2.0.36" - checksum: 10c0/85d8d7f4b6248c8372831cbcc3829ce634cb2b01dbd85e55705cefc8a9eda4ce8121bd218b9629cf2579aef8a360541bad409f3925a35675c825b9471a49d7e9 - languageName: node - linkType: hard - -"nopt@npm:^9.0.0": - version: 9.0.0 - resolution: "nopt@npm:9.0.0" - dependencies: - abbrev: "npm:^4.0.0" - bin: - nopt: bin/nopt.js - checksum: 10c0/1822eb6f9b020ef6f7a7516d7b64a8036e09666ea55ac40416c36e4b2b343122c3cff0e2f085675f53de1d2db99a2a89a60ccea1d120bcd6a5347bf6ceb4a7fd - languageName: node - linkType: hard - -"normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": - version: 3.0.0 - resolution: "normalize-path@npm:3.0.0" - checksum: 10c0/e008c8142bcc335b5e38cf0d63cfd39d6cf2d97480af9abdbe9a439221fd4d749763bab492a8ee708ce7a194bb00c9da6d0a115018672310850489137b3da046 - languageName: node - linkType: hard - -"p-limit@npm:^2.2.0": - version: 2.3.0 - resolution: "p-limit@npm:2.3.0" - dependencies: - p-try: "npm:^2.0.0" - checksum: 10c0/8da01ac53efe6a627080fafc127c873da40c18d87b3f5d5492d465bb85ec7207e153948df6b9cbaeb130be70152f874229b8242ee2be84c0794082510af97f12 - languageName: node - linkType: hard - -"p-limit@npm:^4.0.0": - version: 4.0.0 - resolution: "p-limit@npm:4.0.0" - dependencies: - yocto-queue: "npm:^1.0.0" - checksum: 10c0/a56af34a77f8df2ff61ddfb29431044557fcbcb7642d5a3233143ebba805fc7306ac1d448de724352861cb99de934bc9ab74f0d16fe6a5460bdbdf938de875ad - languageName: node - linkType: hard - -"p-locate@npm:^4.1.0": - version: 4.1.0 - resolution: "p-locate@npm:4.1.0" - dependencies: - p-limit: "npm:^2.2.0" - checksum: 10c0/1b476ad69ad7f6059744f343b26d51ce091508935c1dbb80c4e0a2f397ffce0ca3a1f9f5cd3c7ce19d7929a09719d5c65fe70d8ee289c3f267cd36f2881813e9 - languageName: node - linkType: hard - -"p-locate@npm:^6.0.0": - version: 6.0.0 - resolution: "p-locate@npm:6.0.0" - dependencies: - p-limit: "npm:^4.0.0" - checksum: 10c0/d72fa2f41adce59c198270aa4d3c832536c87a1806e0f69dffb7c1a7ca998fb053915ca833d90f166a8c082d3859eabfed95f01698a3214c20df6bb8de046312 - languageName: node - linkType: hard - -"p-map@npm:^7.0.2": - version: 7.0.4 - resolution: "p-map@npm:7.0.4" - checksum: 10c0/a5030935d3cb2919d7e89454d1ce82141e6f9955413658b8c9403cfe379283770ed3048146b44cde168aa9e8c716505f196d5689db0ae3ce9a71521a2fef3abd - languageName: node - linkType: hard - -"p-try@npm:^2.0.0": - version: 2.2.0 - resolution: "p-try@npm:2.2.0" - checksum: 10c0/c36c19907734c904b16994e6535b02c36c2224d433e01a2f1ab777237f4d86e6289fd5fd464850491e940379d4606ed850c03e0f9ab600b0ebddb511312e177f - languageName: node - linkType: hard - -"path-exists@npm:^4.0.0": - version: 4.0.0 - resolution: "path-exists@npm:4.0.0" - checksum: 10c0/8c0bd3f5238188197dc78dced15207a4716c51cc4e3624c44fc97acf69558f5ebb9a2afff486fe1b4ee148e0c133e96c5e11a9aa5c48a3006e3467da070e5e1b - languageName: node - linkType: hard - -"path-exists@npm:^5.0.0": - version: 5.0.0 - resolution: "path-exists@npm:5.0.0" - checksum: 10c0/b170f3060b31604cde93eefdb7392b89d832dfbc1bed717c9718cbe0f230c1669b7e75f87e19901da2250b84d092989a0f9e44d2ef41deb09aa3ad28e691a40a - languageName: node - linkType: hard - -"path-key@npm:^3.1.0": - version: 3.1.1 - resolution: "path-key@npm:3.1.1" - checksum: 10c0/748c43efd5a569c039d7a00a03b58eecd1d75f3999f5a28303d75f521288df4823bc057d8784eb72358b2895a05f29a070bc9f1f17d28226cc4e62494cc58c4c - languageName: node - linkType: hard - -"path-parse@npm:^1.0.7": - version: 1.0.7 - resolution: "path-parse@npm:1.0.7" - checksum: 10c0/11ce261f9d294cc7a58d6a574b7f1b935842355ec66fba3c3fd79e0f036462eaf07d0aa95bb74ff432f9afef97ce1926c720988c6a7451d8a584930ae7de86e1 - languageName: node - linkType: hard - -"path-scurry@npm:^2.0.2": - version: 2.0.2 - resolution: "path-scurry@npm:2.0.2" - dependencies: - lru-cache: "npm:^11.0.0" - minipass: "npm:^7.1.2" - checksum: 10c0/b35ad37cf6557a87fd057121ce2be7695380c9138d93e87ae928609da259ea0a170fac6f3ef1eb3ece8a068e8b7f2f3adf5bb2374cf4d4a57fe484954fcc9482 - languageName: node - linkType: hard - -"picocolors@npm:^1.1.1": - version: 1.1.1 - resolution: "picocolors@npm:1.1.1" - checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58 - languageName: node - linkType: hard - -"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1": - version: 2.3.1 - resolution: "picomatch@npm:2.3.1" - checksum: 10c0/26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be - languageName: node - linkType: hard - -"picomatch@npm:^4.0.3": - version: 4.0.3 - resolution: "picomatch@npm:4.0.3" - checksum: 10c0/9582c951e95eebee5434f59e426cddd228a7b97a0161a375aed4be244bd3fe8e3a31b846808ea14ef2c8a2527a6eeab7b3946a67d5979e81694654f939473ae2 - languageName: node - linkType: hard - -"pkg-dir@npm:^4.2.0": - version: 4.2.0 - resolution: "pkg-dir@npm:4.2.0" - dependencies: - find-up: "npm:^4.0.0" - checksum: 10c0/c56bda7769e04907a88423feb320babaed0711af8c436ce3e56763ab1021ba107c7b0cafb11cde7529f669cfc22bffcaebffb573645cbd63842ea9fb17cd7728 - languageName: node - linkType: hard - -"pkg-dir@npm:^7.0.0": - version: 7.0.0 - resolution: "pkg-dir@npm:7.0.0" - dependencies: - find-up: "npm:^6.3.0" - checksum: 10c0/1afb23d2efb1ec9d8b2c4a0c37bf146822ad2774f074cb05b853be5dca1b40815c5960dd126df30ab8908349262a266f31b771e877235870a3b8fd313beebec5 - languageName: node - linkType: hard - -"proc-log@npm:^6.0.0": - version: 6.1.0 - resolution: "proc-log@npm:6.1.0" - checksum: 10c0/4f178d4062733ead9d71a9b1ab24ebcecdfe2250916a5b1555f04fe2eda972a0ec76fbaa8df1ad9c02707add6749219d118a4fc46dc56bdfe4dde4b47d80bb82 - languageName: node - linkType: hard - -"readdirp@npm:~3.6.0": - version: 3.6.0 - resolution: "readdirp@npm:3.6.0" - dependencies: - picomatch: "npm:^2.2.1" - checksum: 10c0/6fa848cf63d1b82ab4e985f4cf72bd55b7dcfd8e0a376905804e48c3634b7e749170940ba77b32804d5fe93b3cc521aa95a8d7e7d725f830da6d93f3669ce66b - languageName: node - linkType: hard - -"rechoir@npm:^0.8.0": - version: 0.8.0 - resolution: "rechoir@npm:0.8.0" - dependencies: - resolve: "npm:^1.20.0" - checksum: 10c0/1a30074124a22abbd5d44d802dac26407fa72a0a95f162aa5504ba8246bc5452f8b1a027b154d9bdbabcd8764920ff9333d934c46a8f17479c8912e92332f3ff - languageName: node - linkType: hard - -"reflect-metadata@npm:^0.1.2": - version: 0.1.14 - resolution: "reflect-metadata@npm:0.1.14" - checksum: 10c0/3a6190c7f6cb224f26a012d11f9e329360c01c1945e2cbefea23976a8bacf9db6b794aeb5bf18adcb673c448a234fbc06fc41853c00a6c206b30f0777ecf019e - languageName: node - linkType: hard - -"require-directory@npm:^2.1.1": - version: 2.1.1 - resolution: "require-directory@npm:2.1.1" - checksum: 10c0/83aa76a7bc1531f68d92c75a2ca2f54f1b01463cb566cf3fbc787d0de8be30c9dbc211d1d46be3497dac5785fe296f2dd11d531945ac29730643357978966e99 - languageName: node - linkType: hard - -"require-from-string@npm:^2.0.2": - version: 2.0.2 - resolution: "require-from-string@npm:2.0.2" - checksum: 10c0/aaa267e0c5b022fc5fd4eef49d8285086b15f2a1c54b28240fdf03599cbd9c26049fee3eab894f2e1f6ca65e513b030a7c264201e3f005601e80c49fb2937ce2 - languageName: node - linkType: hard - -"resolve-cwd@npm:^3.0.0": - version: 3.0.0 - resolution: "resolve-cwd@npm:3.0.0" - dependencies: - resolve-from: "npm:^5.0.0" - checksum: 10c0/e608a3ebd15356264653c32d7ecbc8fd702f94c6703ea4ac2fb81d9c359180cba0ae2e6b71faa446631ed6145454d5a56b227efc33a2d40638ac13f8beb20ee4 - languageName: node - linkType: hard - -"resolve-from@npm:^5.0.0": - version: 5.0.0 - resolution: "resolve-from@npm:5.0.0" - checksum: 10c0/b21cb7f1fb746de8107b9febab60095187781137fd803e6a59a76d421444b1531b641bba5857f5dc011974d8a5c635d61cec49e6bd3b7fc20e01f0fafc4efbf2 - languageName: node - linkType: hard - -"resolve@npm:^1.20.0": - version: 1.22.11 - resolution: "resolve@npm:1.22.11" - dependencies: - is-core-module: "npm:^2.16.1" - path-parse: "npm:^1.0.7" - supports-preserve-symlinks-flag: "npm:^1.0.0" - bin: - resolve: bin/resolve - checksum: 10c0/f657191507530f2cbecb5815b1ee99b20741ea6ee02a59c57028e9ec4c2c8d7681afcc35febbd554ac0ded459db6f2d8153382c53a2f266cee2575e512674409 - languageName: node - linkType: hard - -"resolve@patch:resolve@npm%3A^1.20.0#optional!builtin": - version: 1.22.11 - resolution: "resolve@patch:resolve@npm%3A1.22.11#optional!builtin::version=1.22.11&hash=c3c19d" - dependencies: - is-core-module: "npm:^2.16.1" - path-parse: "npm:^1.0.7" - supports-preserve-symlinks-flag: "npm:^1.0.0" - bin: - resolve: bin/resolve - checksum: 10c0/ee5b182f2e37cb1165465e58c6abc797fec0a80b5ba3231607beb4677db0c9291ac010c47cf092b6daa2b7f518d69a0e21888e7e2b633f68d501a874212a8c63 - languageName: node - linkType: hard - -"retry@npm:^0.13.1": - version: 0.13.1 - resolution: "retry@npm:0.13.1" - checksum: 10c0/9ae822ee19db2163497e074ea919780b1efa00431d197c7afdb950e42bf109196774b92a49fc9821f0b8b328a98eea6017410bfc5e8a0fc19c85c6d11adb3772 - languageName: node - linkType: hard - -"rxjs@npm:7.8.2": - version: 7.8.2 - resolution: "rxjs@npm:7.8.2" - dependencies: - tslib: "npm:^2.1.0" - checksum: 10c0/1fcd33d2066ada98ba8f21fcbbcaee9f0b271de1d38dc7f4e256bfbc6ffcdde68c8bfb69093de7eeb46f24b1fb820620bf0223706cff26b4ab99a7ff7b2e2c45 - languageName: node - linkType: hard - -"safer-buffer@npm:>= 2.1.2 < 3.0.0": - version: 2.1.2 - resolution: "safer-buffer@npm:2.1.2" - checksum: 10c0/7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 - languageName: node - linkType: hard - -"schema-utils@npm:^4.0.0, schema-utils@npm:^4.3.0, schema-utils@npm:^4.3.3": - version: 4.3.3 - resolution: "schema-utils@npm:4.3.3" - dependencies: - "@types/json-schema": "npm:^7.0.9" - ajv: "npm:^8.9.0" - ajv-formats: "npm:^2.1.1" - ajv-keywords: "npm:^5.1.0" - checksum: 10c0/1c8d2c480a026d7c02ab2ecbe5919133a096d6a721a3f201fa50663e4f30f6d6ba020dfddd93cb828b66b922e76b342e103edd19a62c95c8f60e9079cc403202 - languageName: node - linkType: hard - -"semver@npm:^6.3.0, semver@npm:^6.3.1": - version: 6.3.1 - resolution: "semver@npm:6.3.1" - bin: - semver: bin/semver.js - checksum: 10c0/e3d79b609071caa78bcb6ce2ad81c7966a46a7431d9d58b8800cfa9cb6a63699b3899a0e4bcce36167a284578212d9ae6942b6929ba4aa5015c079a67751d42d - languageName: node - linkType: hard - -"semver@npm:^7.0.0, semver@npm:^7.3.5": - version: 7.7.4 - resolution: "semver@npm:7.7.4" - bin: - semver: bin/semver.js - checksum: 10c0/5215ad0234e2845d4ea5bb9d836d42b03499546ddafb12075566899fc617f68794bb6f146076b6881d755de17d6c6cc73372555879ec7dce2c2feee947866ad2 - languageName: node - linkType: hard - -"shallow-clone@npm:^3.0.0": - version: 3.0.1 - resolution: "shallow-clone@npm:3.0.1" - dependencies: - kind-of: "npm:^6.0.2" - checksum: 10c0/7bab09613a1b9f480c85a9823aebec533015579fa055ba6634aa56ba1f984380670eaf33b8217502931872aa1401c9fcadaa15f9f604d631536df475b05bcf1e - languageName: node - linkType: hard - -"shebang-command@npm:^2.0.0": - version: 2.0.0 - resolution: "shebang-command@npm:2.0.0" - dependencies: - shebang-regex: "npm:^3.0.0" - checksum: 10c0/a41692e7d89a553ef21d324a5cceb5f686d1f3c040759c50aab69688634688c5c327f26f3ecf7001ebfd78c01f3c7c0a11a7c8bfd0a8bc9f6240d4f40b224e4e - languageName: node - linkType: hard - -"shebang-regex@npm:^3.0.0": - version: 3.0.0 - resolution: "shebang-regex@npm:3.0.0" - checksum: 10c0/1dbed0726dd0e1152a92696c76c7f06084eb32a90f0528d11acd764043aacf76994b2fb30aa1291a21bd019d6699164d048286309a278855ee7bec06cf6fb690 - languageName: node - linkType: hard - -"smart-buffer@npm:^4.2.0": - version: 4.2.0 - resolution: "smart-buffer@npm:4.2.0" - checksum: 10c0/a16775323e1404dd43fabafe7460be13a471e021637bc7889468eb45ce6a6b207261f454e4e530a19500cc962c4cc5348583520843b363f4193cee5c00e1e539 - languageName: node - linkType: hard - -"socks-proxy-agent@npm:^8.0.3": - version: 8.0.5 - resolution: "socks-proxy-agent@npm:8.0.5" - dependencies: - agent-base: "npm:^7.1.2" - debug: "npm:^4.3.4" - socks: "npm:^2.8.3" - checksum: 10c0/5d2c6cecba6821389aabf18728325730504bf9bb1d9e342e7987a5d13badd7a98838cc9a55b8ed3cb866ad37cc23e1086f09c4d72d93105ce9dfe76330e9d2a6 - languageName: node - linkType: hard - -"socks@npm:^2.8.3": - version: 2.8.7 - resolution: "socks@npm:2.8.7" - dependencies: - ip-address: "npm:^10.0.1" - smart-buffer: "npm:^4.2.0" - checksum: 10c0/2805a43a1c4bcf9ebf6e018268d87b32b32b06fbbc1f9282573583acc155860dc361500f89c73bfbb157caa1b4ac78059eac0ef15d1811eb0ca75e0bdadbc9d2 - languageName: node - linkType: hard - -"source-map-support@npm:~0.5.20": - version: 0.5.21 - resolution: "source-map-support@npm:0.5.21" - dependencies: - buffer-from: "npm:^1.0.0" - source-map: "npm:^0.6.0" - checksum: 10c0/9ee09942f415e0f721d6daad3917ec1516af746a8120bba7bb56278707a37f1eb8642bde456e98454b8a885023af81a16e646869975f06afc1a711fb90484e7d - languageName: node - linkType: hard - -"source-map@npm:^0.6.0": - version: 0.6.1 - resolution: "source-map@npm:0.6.1" - checksum: 10c0/ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011 - languageName: node - linkType: hard - -"ssri@npm:^13.0.0": - version: 13.0.1 - resolution: "ssri@npm:13.0.1" - dependencies: - minipass: "npm:^7.0.3" - checksum: 10c0/cf6408a18676c57ff2ed06b8a20dc64bb3e748e5c7e095332e6aecaa2b8422b1e94a739a8453bf65156a8a47afe23757ba4ab52d3ea3b62322dc40875763e17a - languageName: node - linkType: hard - -"string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": - version: 4.2.3 - resolution: "string-width@npm:4.2.3" - dependencies: - emoji-regex: "npm:^8.0.0" - is-fullwidth-code-point: "npm:^3.0.0" - strip-ansi: "npm:^6.0.1" - checksum: 10c0/1e525e92e5eae0afd7454086eed9c818ee84374bb80328fc41217ae72ff5f065ef1c9d7f72da41de40c75fa8bb3dee63d92373fd492c84260a552c636392a47b - languageName: node - linkType: hard - -"strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": - version: 6.0.1 - resolution: "strip-ansi@npm:6.0.1" - dependencies: - ansi-regex: "npm:^5.0.1" - checksum: 10c0/1ae5f212a126fe5b167707f716942490e3933085a5ff6c008ab97ab2f272c8025d3aa218b7bd6ab25729ca20cc81cddb252102f8751e13482a5199e873680952 - languageName: node - linkType: hard - -"supports-color@npm:^8.0.0": - version: 8.1.1 - resolution: "supports-color@npm:8.1.1" - dependencies: - has-flag: "npm:^4.0.0" - checksum: 10c0/ea1d3c275dd604c974670f63943ed9bd83623edc102430c05adb8efc56ba492746b6e95386e7831b872ec3807fd89dd8eb43f735195f37b5ec343e4234cc7e89 - languageName: node - linkType: hard - -"supports-preserve-symlinks-flag@npm:^1.0.0": - version: 1.0.0 - resolution: "supports-preserve-symlinks-flag@npm:1.0.0" - checksum: 10c0/6c4032340701a9950865f7ae8ef38578d8d7053f5e10518076e6554a9381fa91bd9c6850193695c141f32b21f979c985db07265a758867bac95de05f7d8aeb39 - languageName: node - linkType: hard - -"tapable@npm:^2.3.0": - version: 2.3.0 - resolution: "tapable@npm:2.3.0" - checksum: 10c0/cb9d67cc2c6a74dedc812ef3085d9d681edd2c1fa18e4aef57a3c0605fdbe44e6b8ea00bd9ef21bc74dd45314e39d31227aa031ebf2f5e38164df514136f2681 - languageName: node - linkType: hard - -"tar@npm:^7.5.4": - version: 7.5.11 - resolution: "tar@npm:7.5.11" - dependencies: - "@isaacs/fs-minipass": "npm:^4.0.0" - chownr: "npm:^3.0.0" - minipass: "npm:^7.1.2" - minizlib: "npm:^3.1.0" - yallist: "npm:^5.0.0" - checksum: 10c0/b6bb420550ef50ef23356018155e956cd83282c97b6128d8d5cfe5740c57582d806a244b2ef0bf686a74ce526babe8b8b9061527623e935e850008d86d838929 - languageName: node - linkType: hard - -"terser-webpack-plugin@npm:^5.3.16": - version: 5.4.0 - resolution: "terser-webpack-plugin@npm:5.4.0" - dependencies: - "@jridgewell/trace-mapping": "npm:^0.3.25" - jest-worker: "npm:^27.4.5" - schema-utils: "npm:^4.3.0" - terser: "npm:^5.31.1" - peerDependencies: - webpack: ^5.1.0 - peerDependenciesMeta: - "@swc/core": - optional: true - esbuild: - optional: true - uglify-js: - optional: true - checksum: 10c0/1feed4b9575af795dae6af0c8f0d76d6e1fb7b357b8628d90e834c23a651b918a58cdc48d0ae6c1f0581f74bc8169b33c3b8d049f2d2190bac4e310964e59fde - languageName: node - linkType: hard - -"terser@npm:^5.31.1": - version: 5.46.0 - resolution: "terser@npm:5.46.0" - dependencies: - "@jridgewell/source-map": "npm:^0.3.3" - acorn: "npm:^8.15.0" - commander: "npm:^2.20.0" - source-map-support: "npm:~0.5.20" - bin: - terser: bin/terser - checksum: 10c0/93ad468f13187c4f66b609bbfc00a6aee752007779ca3157f2c1ee063697815748d6010fd449a16c30be33213748431d5f54cc0224ba6a3fbbf5acd3582a4356 - languageName: node - linkType: hard - -"tinyglobby@npm:^0.2.12": - version: 0.2.15 - resolution: "tinyglobby@npm:0.2.15" - dependencies: - fdir: "npm:^6.5.0" - picomatch: "npm:^4.0.3" - checksum: 10c0/869c31490d0d88eedb8305d178d4c75e7463e820df5a9b9d388291daf93e8b1eb5de1dad1c1e139767e4269fe75f3b10d5009b2cc14db96ff98986920a186844 - languageName: node - linkType: hard - -"to-regex-range@npm:^5.0.1": - version: 5.0.1 - resolution: "to-regex-range@npm:5.0.1" - dependencies: - is-number: "npm:^7.0.0" - checksum: 10c0/487988b0a19c654ff3e1961b87f471702e708fa8a8dd02a298ef16da7206692e8552a0250e8b3e8759270f62e9d8314616f6da274734d3b558b1fc7b7724e892 - languageName: node - linkType: hard - -"tslib@npm:^2.1.0, tslib@npm:^2.3.0": - version: 2.8.1 - resolution: "tslib@npm:2.8.1" - checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 - languageName: node - linkType: hard - -"typescript@npm:4.9.5": - version: 4.9.5 - resolution: "typescript@npm:4.9.5" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10c0/5f6cad2e728a8a063521328e612d7876e12f0d8a8390d3b3aaa452a6a65e24e9ac8ea22beb72a924fd96ea0a49ea63bb4e251fb922b12eedfb7f7a26475e5c56 - languageName: node - linkType: hard - -"typescript@patch:typescript@npm%3A4.9.5#optional!builtin": - version: 4.9.5 - resolution: "typescript@patch:typescript@npm%3A4.9.5#optional!builtin::version=4.9.5&hash=289587" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10c0/e3333f887c6829dfe0ab6c1dbe0dd1e3e2aeb56c66460cb85c5440c566f900c833d370ca34eb47558c0c69e78ced4bfe09b8f4f98b6de7afed9b84b8d1dd06a1 - languageName: node - linkType: hard - -"undici-types@npm:~7.18.0": - version: 7.18.2 - resolution: "undici-types@npm:7.18.2" - checksum: 10c0/85a79189113a238959d7a647368e4f7c5559c3a404ebdb8fc4488145ce9426fcd82252a844a302798dfc0e37e6fb178ff481ed03bc4caf634c5757d9ef43521d - languageName: node - linkType: hard - -"unique-filename@npm:^5.0.0": - version: 5.0.0 - resolution: "unique-filename@npm:5.0.0" - dependencies: - unique-slug: "npm:^6.0.0" - checksum: 10c0/afb897e9cf4c2fb622ea716f7c2bb462001928fc5f437972213afdf1cc32101a230c0f1e9d96fc91ee5185eca0f2feb34127145874975f347be52eb91d6ccc2c - languageName: node - linkType: hard - -"unique-slug@npm:^6.0.0": - version: 6.0.0 - resolution: "unique-slug@npm:6.0.0" - dependencies: - imurmurhash: "npm:^0.1.4" - checksum: 10c0/da7ade4cb04eb33ad0499861f82fe95ce9c7c878b7139dc54d140ecfb6a6541c18a5c8dac16188b8b379fe62c0c1f1b710814baac910cde5f4fec06212126c6a - languageName: node - linkType: hard - -"update-browserslist-db@npm:^1.2.0": - version: 1.2.3 - resolution: "update-browserslist-db@npm:1.2.3" - dependencies: - escalade: "npm:^3.2.0" - picocolors: "npm:^1.1.1" - peerDependencies: - browserslist: ">= 4.21.0" - bin: - update-browserslist-db: cli.js - checksum: 10c0/13a00355ea822388f68af57410ce3255941d5fb9b7c49342c4709a07c9f230bbef7f7499ae0ca7e0de532e79a82cc0c4edbd125f1a323a1845bf914efddf8bec - languageName: node - linkType: hard - -"watchpack@npm:^2.5.1": - version: 2.5.1 - resolution: "watchpack@npm:2.5.1" - dependencies: - glob-to-regexp: "npm:^0.4.1" - graceful-fs: "npm:^4.1.2" - checksum: 10c0/dffbb483d1f61be90dc570630a1eb308581e2227d507d783b1d94a57ac7b705ecd9a1a4b73d73c15eab596d39874e5276a3d9cb88bbb698bafc3f8d08c34cf17 - languageName: node - linkType: hard - -"webpack-cli@npm:6.0.1": - version: 6.0.1 - resolution: "webpack-cli@npm:6.0.1" - dependencies: - "@discoveryjs/json-ext": "npm:^0.6.1" - "@webpack-cli/configtest": "npm:^3.0.1" - "@webpack-cli/info": "npm:^3.0.1" - "@webpack-cli/serve": "npm:^3.0.1" - colorette: "npm:^2.0.14" - commander: "npm:^12.1.0" - cross-spawn: "npm:^7.0.3" - envinfo: "npm:^7.14.0" - fastest-levenshtein: "npm:^1.0.12" - import-local: "npm:^3.0.2" - interpret: "npm:^3.1.1" - rechoir: "npm:^0.8.0" - webpack-merge: "npm:^6.0.1" - peerDependencies: - webpack: ^5.82.0 - peerDependenciesMeta: - webpack-bundle-analyzer: - optional: true - webpack-dev-server: - optional: true - bin: - webpack-cli: ./bin/cli.js - checksum: 10c0/2aaca78e277427f03f528602abd707d224696048fb46286ea636c7975592409c4381ca94d68bbbb3900f195ca97f256e619583e8feb34a80da531461323bf3e2 - languageName: node - linkType: hard - -"webpack-merge@npm:^6.0.1": - version: 6.0.1 - resolution: "webpack-merge@npm:6.0.1" - dependencies: - clone-deep: "npm:^4.0.1" - flat: "npm:^5.0.2" - wildcard: "npm:^2.0.1" - checksum: 10c0/bf1429567858b353641801b8a2696ca0aac270fc8c55d4de8a7b586fe07d27fdcfc83099a98ab47e6162383db8dd63bb8cc25b1beb2ec82150422eec843b0dc0 - languageName: node - linkType: hard - -"webpack-sources@npm:^3.3.3": - version: 3.3.4 - resolution: "webpack-sources@npm:3.3.4" - checksum: 10c0/94a42508531338eb41939cf1d48a4a8a6db97f3a47e5453cff2133a68d3169ca779d4bcbe9dfed072ce16611959eba1e16f085bc2dc56714e1a1c1783fd661a3 - languageName: node - linkType: hard - -"webpack@npm:5.105.2": - version: 5.105.2 - resolution: "webpack@npm:5.105.2" - dependencies: - "@types/eslint-scope": "npm:^3.7.7" - "@types/estree": "npm:^1.0.8" - "@types/json-schema": "npm:^7.0.15" - "@webassemblyjs/ast": "npm:^1.14.1" - "@webassemblyjs/wasm-edit": "npm:^1.14.1" - "@webassemblyjs/wasm-parser": "npm:^1.14.1" - acorn: "npm:^8.15.0" - acorn-import-phases: "npm:^1.0.3" - browserslist: "npm:^4.28.1" - chrome-trace-event: "npm:^1.0.2" - enhanced-resolve: "npm:^5.19.0" - es-module-lexer: "npm:^2.0.0" - eslint-scope: "npm:5.1.1" - events: "npm:^3.2.0" - glob-to-regexp: "npm:^0.4.1" - graceful-fs: "npm:^4.2.11" - json-parse-even-better-errors: "npm:^2.3.1" - loader-runner: "npm:^4.3.1" - mime-types: "npm:^2.1.27" - neo-async: "npm:^2.6.2" - schema-utils: "npm:^4.3.3" - tapable: "npm:^2.3.0" - terser-webpack-plugin: "npm:^5.3.16" - watchpack: "npm:^2.5.1" - webpack-sources: "npm:^3.3.3" - peerDependenciesMeta: - webpack-cli: - optional: true - bin: - webpack: bin/webpack.js - checksum: 10c0/565df8072c00d72e0a22e136971862b7eac7beb8b8d39a2ae4ab00838941ea58acc5b49dd7ea268e3d839810756cb86ba5c272b3a25904f6db7807cfa8ed0b29 - languageName: node - linkType: hard - -"which@npm:^2.0.1": - version: 2.0.2 - resolution: "which@npm:2.0.2" - dependencies: - isexe: "npm:^2.0.0" - bin: - node-which: ./bin/node-which - checksum: 10c0/66522872a768b60c2a65a57e8ad184e5372f5b6a9ca6d5f033d4b0dc98aff63995655a7503b9c0a2598936f532120e81dd8cc155e2e92ed662a2b9377cc4374f - languageName: node - linkType: hard - -"which@npm:^6.0.0": - version: 6.0.1 - resolution: "which@npm:6.0.1" - dependencies: - isexe: "npm:^4.0.0" - bin: - node-which: bin/which.js - checksum: 10c0/7e710e54ea36d2d6183bee2f9caa27a3b47b9baf8dee55a199b736fcf85eab3b9df7556fca3d02b50af7f3dfba5ea3a45644189836df06267df457e354da66d5 - languageName: node - linkType: hard - -"wildcard@npm:^2.0.1": - version: 2.0.1 - resolution: "wildcard@npm:2.0.1" - checksum: 10c0/08f70cd97dd9a20aea280847a1fe8148e17cae7d231640e41eb26d2388697cbe65b67fd9e68715251c39b080c5ae4f76d71a9a69fa101d897273efdfb1b58bf7 - languageName: node - linkType: hard - -"wrap-ansi@npm:^7.0.0": - version: 7.0.0 - resolution: "wrap-ansi@npm:7.0.0" - dependencies: - ansi-styles: "npm:^4.0.0" - string-width: "npm:^4.1.0" - strip-ansi: "npm:^6.0.0" - checksum: 10c0/d15fc12c11e4cbc4044a552129ebc75ee3f57aa9c1958373a4db0292d72282f54373b536103987a4a7594db1ef6a4f10acf92978f79b98c49306a4b58c77d4da - languageName: node - linkType: hard - -"y18n@npm:^5.0.5": - version: 5.0.8 - resolution: "y18n@npm:5.0.8" - checksum: 10c0/4df2842c36e468590c3691c894bc9cdbac41f520566e76e24f59401ba7d8b4811eb1e34524d57e54bc6d864bcb66baab7ffd9ca42bf1eda596618f9162b91249 - languageName: node - linkType: hard - -"yallist@npm:^3.0.2": - version: 3.1.1 - resolution: "yallist@npm:3.1.1" - checksum: 10c0/c66a5c46bc89af1625476f7f0f2ec3653c1a1791d2f9407cfb4c2ba812a1e1c9941416d71ba9719876530e3340a99925f697142989371b72d93b9ee628afd8c1 - languageName: node - linkType: hard - -"yallist@npm:^4.0.0": - version: 4.0.0 - resolution: "yallist@npm:4.0.0" - checksum: 10c0/2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a - languageName: node - linkType: hard - -"yallist@npm:^5.0.0": - version: 5.0.0 - resolution: "yallist@npm:5.0.0" - checksum: 10c0/a499c81ce6d4a1d260d4ea0f6d49ab4da09681e32c3f0472dee16667ed69d01dae63a3b81745a24bd78476ec4fcf856114cb4896ace738e01da34b2c42235416 - languageName: node - linkType: hard - -"yargs-parser@npm:^21.1.1": - version: 21.1.1 - resolution: "yargs-parser@npm:21.1.1" - checksum: 10c0/f84b5e48169479d2f402239c59f084cfd1c3acc197a05c59b98bab067452e6b3ea46d4dd8ba2985ba7b3d32a343d77df0debd6b343e5dae3da2aab2cdf5886b2 - languageName: node - linkType: hard - -"yargs@npm:^17.2.1": - version: 17.7.2 - resolution: "yargs@npm:17.7.2" - dependencies: - cliui: "npm:^8.0.1" - escalade: "npm:^3.1.1" - get-caller-file: "npm:^2.0.5" - require-directory: "npm:^2.1.1" - string-width: "npm:^4.2.3" - y18n: "npm:^5.0.5" - yargs-parser: "npm:^21.1.1" - checksum: 10c0/ccd7e723e61ad5965fffbb791366db689572b80cca80e0f96aad968dfff4156cd7cd1ad18607afe1046d8241e6fb2d6c08bf7fa7bfb5eaec818735d8feac8f05 - languageName: node - linkType: hard - -"yocto-queue@npm:^1.0.0": - version: 1.2.2 - resolution: "yocto-queue@npm:1.2.2" - checksum: 10c0/36d4793e9cf7060f9da543baf67c55e354f4862c8d3d34de1a1b1d7c382d44171315cc54abf84d8900b8113d742b830108a1434f4898fb244f9b7e8426d4b8f5 - languageName: node - linkType: hard - -"zone.js@npm:0.12.0": - version: 0.12.0 - resolution: "zone.js@npm:0.12.0" - dependencies: - tslib: "npm:^2.3.0" - checksum: 10c0/827efca5e0139644882aef6fa2c26c38beba5fa488613fc0a98d8bdd103d35b73b89d8a4b9d9de4ebab1f9cde32b17b9e19aff0824883af427988e5043621351 - languageName: node - linkType: hard diff --git a/test/e2e/lib/framework/sdkBuilds.ts b/test/e2e/lib/framework/sdkBuilds.ts index 58652b5140..a4baf98ebd 100644 --- a/test/e2e/lib/framework/sdkBuilds.ts +++ b/test/e2e/lib/framework/sdkBuilds.ts @@ -11,7 +11,6 @@ export function getTestAppBundlePath(appName: string, originalUrl: string) { app: 'apps/vanilla', 'react-router-v6-app': 'apps/react-router-v6-app', 'react-router-v7-app': 'apps/react-router-v7-app', - 'angular-app': 'apps/angular-app', microfrontend: 'apps/microfrontend', } diff --git a/test/e2e/lib/framework/serverApps/mock.ts b/test/e2e/lib/framework/serverApps/mock.ts index dbeaaa2279..e0cd2a217f 100644 --- a/test/e2e/lib/framework/serverApps/mock.ts +++ b/test/e2e/lib/framework/serverApps/mock.ts @@ -203,7 +203,7 @@ export function createMockServerApp(servers: Servers, setup: string, setupOption } }) - app.get(/(?app|react-[\w-]+|angular-[\w-]+).js$/, (req, res) => { + app.get(/(?app|react-[\w-]+).js$/, (req, res) => { const { originalUrl, params } = req res.sendFile(getTestAppBundlePath(params.appName, originalUrl)) }) diff --git a/test/e2e/scenario/angularPlugin.scenario.ts b/test/e2e/scenario/angularPlugin.scenario.ts deleted file mode 100644 index 3a7f4b5b8e..0000000000 --- a/test/e2e/scenario/angularPlugin.scenario.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { test, expect } from '@playwright/test' -import { createTest } from '../lib/framework' - -test.describe('angular plugin', () => { - createTest('should define a view name based on the route') - .withRum() - .withApp('angular-app') - .run(async ({ page, flushEvents, intakeRegistry }) => { - await page.click('text=Go to Parameterized Route') - await flushEvents() - const viewEvents = intakeRegistry.rumViewEvents - expect(viewEvents.length).toBeGreaterThan(0) - const lastView = viewEvents[viewEvents.length - 1] - expect(lastView.view.name).toBe('/parameterized/:id') - expect(lastView.view.url).toContain('/parameterized/42') - }) - - createTest('should define a view name for nested routes') - .withRum() - .withApp('angular-app') - .run(async ({ page, flushEvents, intakeRegistry }) => { - await page.click('text=Go to Nested Route') - await flushEvents() - const viewEvents = intakeRegistry.rumViewEvents - expect(viewEvents.length).toBeGreaterThan(0) - const lastView = viewEvents[viewEvents.length - 1] - expect(lastView.view.name).toBe('/parent/nested') - expect(lastView.view.url).toContain('/parent/nested') - }) - - createTest('should define a view name with the actual path for wildcard routes') - .withRum() - .withApp('angular-app') - .run(async ({ page, flushEvents, intakeRegistry }) => { - await page.click('text=Go to Wildcard Route') - await flushEvents() - const viewEvents = intakeRegistry.rumViewEvents - expect(viewEvents.length).toBeGreaterThan(0) - const lastView = viewEvents[viewEvents.length - 1] - expect(lastView.view.name).toBe('/unknown/page') - expect(lastView.view.url).toContain('/unknown/page') - }) - - createTest('should define a view name for the initial route') - .withRum() - .withApp('angular-app') - .run(async ({ flushEvents, intakeRegistry }) => { - await flushEvents() - const viewEvents = intakeRegistry.rumViewEvents - expect(viewEvents.length).toBeGreaterThan(0) - const firstView = viewEvents[0] - expect(firstView.view.name).toBe('/') - }) - - createTest('should not create a new view on query param changes') - .withRum() - .withApp('angular-app') - .run(async ({ page, flushEvents, intakeRegistry }) => { - await page.click('#query-param-link') - await flushEvents() - - const viewEvents = intakeRegistry.rumViewEvents - // Only the initial view should exist — the query param change should not create a new one - expect(viewEvents).toHaveLength(1) - }) - - createTest('should report errors caught by provideDatadogErrorHandler') - .withRum() - .withApp('angular-app') - .run(async ({ page, flushEvents, intakeRegistry, withBrowserLogs }) => { - await page.click('#throw-error') - await flushEvents() - - const angularErrors = intakeRegistry.rumErrorEvents.filter((event) => event.context?.framework === 'angular') - expect(angularErrors).toHaveLength(1) - expect(angularErrors[0].error.message).toBe('angular error from component') - expect(angularErrors[0].error.handling).toBe('handled') - expect(angularErrors[0].error.source).toBe('custom') - expect(angularErrors[0].error.handling_stack).toEqual(expect.stringContaining('angular error')) - - withBrowserLogs((browserLogs) => { - expect(browserLogs.filter((log) => log.level === 'error').length).toBeGreaterThan(0) - }) - }) - - createTest('should merge dd_context from the error object into the event context') - .withRum() - .withApp('angular-app') - .run(async ({ page, flushEvents, intakeRegistry, withBrowserLogs }) => { - await page.click('#throw-error-with-context') - await flushEvents() - - const angularErrors = intakeRegistry.rumErrorEvents.filter((event) => event.context?.framework === 'angular') - expect(angularErrors).toHaveLength(1) - expect(angularErrors[0].error.message).toBe('angular error with dd_context') - expect(angularErrors[0].context).toEqual( - expect.objectContaining({ - framework: 'angular', - component: 'InitialRoute', - userId: 42, - }) - ) - - withBrowserLogs((browserLogs) => { - expect(browserLogs.filter((log) => log.level === 'error').length).toBeGreaterThan(0) - }) - }) -}) diff --git a/test/unit/globalThisPolyfill.js b/test/unit/globalThisPolyfill.js deleted file mode 100644 index cd39cadf99..0000000000 --- a/test/unit/globalThisPolyfill.js +++ /dev/null @@ -1,6 +0,0 @@ -// Polyfill globalThis for browsers that don't support it (e.g. Chrome < 71) -// Required because @angular/core uses globalThis internally. -/* eslint-disable no-undef */ -if (typeof globalThis === 'undefined') { - window.globalThis = window -} diff --git a/test/unit/karma.base.conf.js b/test/unit/karma.base.conf.js index f5cc511058..1e0970f7d3 100644 --- a/test/unit/karma.base.conf.js +++ b/test/unit/karma.base.conf.js @@ -21,9 +21,6 @@ if (testReportDirectory) { } const FILES = [ - // Polyfill globalThis for older browsers (e.g. Chrome 63) that don't support it. - // Required because @angular/core uses globalThis internally. - { pattern: 'test/unit/globalThisPolyfill.js', watched: false }, // Make sure 'forEach.spec' is the first file to be loaded, so its `beforeEach` hook is executed // before all other `beforeEach` hooks, and its `afterEach` hook is executed after all other // `afterEach` hooks. @@ -130,7 +127,7 @@ function overrideTsLoaderRule(module) { module.rules.push({ test: /\.m?js$/, include: - /node_modules\/(@angular\/core|react|react-router-dom|react-dom|react-router|turbo-stream|vue-router|@vue\/test-utils)/, + /node_modules\/(react|react-router-dom|react-dom|react-router|turbo-stream|vue-router|@vue\/test-utils)/, use: { loader: 'swc-loader', options: { diff --git a/yarn.lock b/yarn.lock index 64169689d4..52215219a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34,53 +34,6 @@ __metadata: languageName: node linkType: hard -"@angular/common@npm:19.2.5": - version: 19.2.5 - resolution: "@angular/common@npm:19.2.5" - dependencies: - tslib: "npm:^2.3.0" - peerDependencies: - "@angular/core": 19.2.5 - rxjs: ^6.5.3 || ^7.4.0 - checksum: 10c0/0aed8a84a6655215948a2d739f7c8059aa1e9f2e83eac9220a76f776b47401538b26d1c35695926a262c7d838a96accf56a72a0611c77e0335cfed56eef132bf - languageName: node - linkType: hard - -"@angular/compiler@npm:19.2.5": - version: 19.2.5 - resolution: "@angular/compiler@npm:19.2.5" - dependencies: - tslib: "npm:^2.3.0" - checksum: 10c0/e65f84ef2d53eb1250d3085074eb1b3fbd8d3f8f9603e32be67c82194ae4f8a4a98f250d954f830a67e68c4d2f3b3d01da5c0f49157cc1746064a37d14b74a76 - languageName: node - linkType: hard - -"@angular/core@npm:19.2.5": - version: 19.2.5 - resolution: "@angular/core@npm:19.2.5" - dependencies: - tslib: "npm:^2.3.0" - peerDependencies: - rxjs: ^6.5.3 || ^7.4.0 - zone.js: ~0.15.0 - checksum: 10c0/102efc14698c9adea29af9ddc81297e8d41d061e3148a640713c2cdff0f91cf43b161b02790ab21f1bb551a0685d023b5a63e30740a62dcfde8a4c6e6c41d496 - languageName: node - linkType: hard - -"@angular/router@npm:19.2.5": - version: 19.2.5 - resolution: "@angular/router@npm:19.2.5" - dependencies: - tslib: "npm:^2.3.0" - peerDependencies: - "@angular/common": 19.2.5 - "@angular/core": 19.2.5 - "@angular/platform-browser": 19.2.5 - rxjs: ^6.5.3 || ^7.4.0 - checksum: 10c0/5eecadb52552633603fce5e5415c37275eb44451d2591fb78f9dd85fc5015a1d65014989b89e5fbdf9eacfc39420804b11c9d4774c28f3508bce4fee129c9f26 - languageName: node - linkType: hard - "@apidevtools/json-schema-ref-parser@npm:^11.5.5": version: 11.9.3 resolution: "@apidevtools/json-schema-ref-parser@npm:11.9.3" @@ -339,24 +292,6 @@ __metadata: languageName: unknown linkType: soft -"@datadog/browser-rum-angular@workspace:packages/rum-angular": - version: 0.0.0-use.local - resolution: "@datadog/browser-rum-angular@workspace:packages/rum-angular" - dependencies: - "@angular/common": "npm:19.2.5" - "@angular/compiler": "npm:19.2.5" - "@angular/core": "npm:19.2.5" - "@angular/router": "npm:19.2.5" - "@datadog/browser-core": "npm:6.32.0" - "@datadog/browser-rum-core": "npm:6.32.0" - rxjs: "npm:7.8.2" - peerDependencies: - "@angular/core": ">=15 <=21" - "@angular/router": ">=15 <=21" - rxjs: ">=7" - languageName: unknown - linkType: soft - "@datadog/browser-rum-core@npm:6.32.0, @datadog/browser-rum-core@workspace:*, @datadog/browser-rum-core@workspace:packages/rum-core": version: 0.0.0-use.local resolution: "@datadog/browser-rum-core@workspace:packages/rum-core" @@ -10552,15 +10487,6 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:7.8.2": - version: 7.8.2 - resolution: "rxjs@npm:7.8.2" - dependencies: - tslib: "npm:^2.1.0" - checksum: 10c0/1fcd33d2066ada98ba8f21fcbbcaee9f0b271de1d38dc7f4e256bfbc6ffcdde68c8bfb69093de7eeb46f24b1fb820620bf0223706cff26b4ab99a7ff7b2e2c45 - languageName: node - linkType: hard - "safe-array-concat@npm:^1.1.3": version: 1.1.3 resolution: "safe-array-concat@npm:1.1.3" @@ -11758,7 +11684,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.4.0, tslib@npm:^2.8.0": +"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.8.0": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 From c67019b9e18d9b65020ccffd81e631254438fef4 Mon Sep 17 00:00:00 2001 From: Aymeric Mortemousque Date: Fri, 17 Apr 2026 14:20:47 +0200 Subject: [PATCH 09/12] use more deterministic approach --- .claude/skills/router-design/SKILL.md | 163 +++++-------- .../skills/router-design/output.schema.json | 111 +++++++++ .claude/skills/router-experiment/SKILL.md | 140 +++++++++++ .claude/skills/router-fetch-docs/SKILL.md | 112 +++------ .../router-fetch-docs/output.schema.json | 224 ++++++++++++++++++ .claude/skills/router-generate/SKILL.md | 186 +++++++++++---- .../skills/router-generate/output.template.md | 27 +++ .claude/skills/router-pipeline/SKILL.md | 107 ++++----- .claude/skills/router-pr/SKILL.md | 39 ++- 9 files changed, 807 insertions(+), 302 deletions(-) create mode 100644 .claude/skills/router-design/output.schema.json create mode 100644 .claude/skills/router-experiment/SKILL.md create mode 100644 .claude/skills/router-fetch-docs/output.schema.json create mode 100644 .claude/skills/router-generate/output.template.md diff --git a/.claude/skills/router-design/SKILL.md b/.claude/skills/router-design/SKILL.md index 8994179e87..d19a53148b 100644 --- a/.claude/skills/router-design/SKILL.md +++ b/.claude/skills/router-design/SKILL.md @@ -1,145 +1,106 @@ --- name: router-design -description: 'Stage 2: Analyze reference implementations and produce design decisions document from router concepts. Reads 01-router-concepts.md and reference code.' +description: 'Stage 2: Analyze reference implementations and produce design decisions from the stage 1 JSON. Reads 01-router-concepts.json and reference code, emits JSON conforming to output.schema.json.' --- # Stage 2: Design Decisions ## Context -You are Stage 2 of the router integration pipeline. Your job is to read the router concepts extracted in Stage 1, analyze the existing reference implementations, and produce a concrete design document that will guide code generation. +You are Stage 2 of the router integration pipeline. Your job is to read the structured router concepts from Stage 1, analyze the existing reference implementations, and produce explicit design decisions that will guide code generation in Stage 3. -## Input - -1. Read `docs/integrations//01-router-concepts.md` -2. Read the reference implementations to understand the SDK contract: - - Plugin interface: `packages/rum-core/src/domain/plugins.ts` - - Public API: `packages/rum-core/src/boot/rumPublicApi.ts` - - Angular router: `packages/rum-angular/src/domain/angularRouter/` (all files) - - React router: `packages/rum-react/src/domain/reactRouter/` (all files) - - Vue router: `packages/rum-vue/src/domain/router/` (all files) -3. Read reference entry points and package configs: - - Angular entry point: `packages/rum-angular/src/entries/main.ts` - - Vue entry point: `packages/rum-vue/src/entries/main.ts` - - React entry point: `packages/rum-react/src/entries/main.ts` - - Vue package.json: `packages/rum-vue/package.json` - - Angular package.json: `packages/rum-angular/package.json` - -Find the `` directory by listing `docs/integrations/`. - -## Process +The pipeline invokes you with `claude -p --output-format json --json-schema .claude/skills/router-design/output.schema.json`. Your final message must be a single JSON object conforming to that schema; the harness validates it and writes the CLI wrapper to `docs/integrations//02-design-decisions.json` with the payload on `.structured_output`. -For each concept in `01-router-concepts.md`, find the closest equivalent in the reference implementations. Every mapping MUST include inline links to both: - -- The framework doc source (from 01-router-concepts.md links) -- The specific file and line range in the reference implementation - -### Required Sections +## Input -**Architecture Overview** -2-3 sentences describing the overall approach. Which reference implementation is closest and why. +You receive a **framework identifier** as skill param (e.g. `angular`, `vue`, `tanstack-react-router`). -**Public API** -Exactly what the user imports and calls. Show the complete setup code example: +Read: -```typescript -// What the user writes in their app -import { ... } from '@datadog/browser-rum-' -``` +1. `docs/integrations//01-router-concepts.json` — stage 1 CLI wrapper. Extract the router-concepts payload with `jq '.structured_output' `. +2. Reference implementations (to understand SDK patterns): + - Plugin files: `packages/rum-vue/src/domain/vuePlugin.ts`, `packages/rum-react/src/domain/reactPlugin.ts`, `packages/rum-nextjs/src/domain/nextjsPlugin.ts` + - Vue router: `packages/rum-vue/src/domain/router/` (all `.ts` files) + - React router: `packages/rum-react/src/domain/reactRouter/` (all `.ts` files) + - Next.js router: `packages/rum-nextjs/src/domain/nextJSRouter/` (all `.ts` files) + - Entry points: `packages/rum-vue/src/entries/main.ts`, `packages/rum-react/src/entries/main.ts`, `packages/rum-nextjs/src/entries/main.ts` + - Package configs: `packages/rum-vue/package.json`, `packages/rum-react/package.json`, `packages/rum-nextjs/package.json` + - Plugin interface: `packages/rum-core/src/domain/plugins.ts` -**File Structure** -The exact file tree for `packages/rum-/` with one-line descriptions per file. Follow the convention from reference packages: +## Process -- `src/entries/main.ts` — public exports -- `src/domain/Plugin.ts` — plugin + subscriber pattern -- `src/domain/Router/` — router integration files -- `src/test/` — test helpers -- `package.json`, `tsconfig.json`, `README.md` +### 1. Hook Selection (Deterministic) -**Navigation Hook Decision** -Which framework hook/event to subscribe to, and which reference implementation it's most similar to. Justify the choice based on the lifecycle timing analysis from Stage 1 (after redirects, before data fetches, before render). +Apply these priority rules to the `hooks` array from `01-router-concepts.json`. -IMPORTANT: The navigation hook choice directly affects what data is available to `computeViewName()`. Different hooks may expose different route objects, matched route arrays, or URL representations. If the framework has multiple candidate hooks, show how the view name computation differs for each option: +**The integration must be client-side only.** Only consider hooks that fire on the client. Use the `access` field and `ssr` section from `01-router-concepts.json` to determine this. -- What route data each hook provides (e.g. matched route records vs. raw URL vs. route config) -- How that changes the `computeViewName()` implementation -- Whether one hook gives better data for view name computation (e.g. access to parameterized route patterns vs. only resolved URLs) +**Priority rules (in order):** -This analysis should reinforce or challenge the hook choice — if a hook that fires later provides significantly better route data, that trade-off must be documented. +1. `afterCancellation: true` — **required**. Never start a RUM view for a navigation that didn't occur. +2. `afterRedirects: true` — **prefer**. Report the final destination, not intermediate routes. +3. `afterFetch: false` AND `afterRender: false` — **prefer**. Start the view before data loading and DOM mutation so RUM events (fetch resources, long tasks, interactions) are attributed to the new view, not the previous one. -**View Name Algorithm** -Pseudocode or step-by-step description of how `computeViewName()` will work for this framework. Cover: +Apply in order: -- How to access the matched route records after navigation -- How dynamic segments appear in the route definition (and whether they need normalization) -- How catch-all/wildcard routes should be substituted with actual path segments -- Normal routes, dynamic segments, nested routes, catch-all/wildcard routes -- Edge cases specific to this framework -- Link to the most similar existing `computeViewName` implementation and note differences +- Filter to `afterCancellation: true`. If no hooks pass, flag as critical issue and stop. +- Among those, prefer `afterRedirects: true`. +- Among those, prefer `afterFetch: false` AND `afterRender: false`. +- If rules conflict (no hook satisfies all), higher-priority rule wins. +- If multiple hooks still tie, prefer the one that fires earliest in the lifecycle. -**Wrapping Strategy** -How the integration hooks into the framework. Reference implementations: +Document which hooks were considered, which rules each passed/failed, and why the selected hook won. -- Angular: [`ENVIRONMENT_INITIALIZER` provider](packages/rum-angular/src/domain/angularRouter/provideDatadogRouter.ts) -- React: [wrapper around `createRouter`](packages/rum-react/src/domain/reactRouter/createRouter.ts) and [`useRoutes` hook](packages/rum-react/src/domain/reactRouter/useRoutes.ts) -- Vue: [wrapper around `createRouter`](packages/rum-vue/src/domain/router/vueRouter.ts) +### 2. Wrapping Strategy (LLM Judgment) -Determine which pattern fits this framework and why. +Read the selected hook's `access` field from `01-router-concepts.json`. Determine the most idiomatic way for users to integrate the plugin in this framework. -**Type Strategy** -Whether to define minimal local types (like Angular's [`RouteSnapshot`](packages/rum-angular/src/domain/angularRouter/types.ts)) to avoid runtime framework imports, or import types directly from the framework package. +Consider: -**Plugin Configuration** -How the plugin will be configured. All reference implementations use the same pattern: +- How existing plugins/libraries are typically added in this framework's ecosystem +- Whether the hook needs a router instance (→ wrap the factory that creates it) +- Whether the hook needs component context (→ renderless component or hook) +- Whether the hook needs DI (→ provider registration) -- [`VuePluginConfiguration`](packages/rum-vue/src/domain/vuePlugin.ts) with `router?: boolean` -- `onInit` sets `trackViewsManually = true` when `router: true` +Reference patterns from existing implementations: -**Peer Dependencies** -Which framework packages are required as peer dependencies, with version ranges. +- Vue: wraps `createRouter()` factory to get router instance for `afterEach` +- React: wraps `createBrowserRouter()` factory OR wraps `useRoutes()` hook +- Angular: provider with `inject(Router)` for `router.events` observable -**Navigation Filtering** -How to handle: +### 3. View Name Algorithm (LLM Classification) -- Failed navigations (guards blocking, cancellations) -- Duplicate navigations (same path) -- Query-only changes -- Initial navigation +Read the selected hook's `availableApi` from `01-router-concepts.json`. Classify into one of three families (in preference order): -Reference the filtering logic in existing implementations: +- **`route-id`** — Framework provides the parameterized route pattern as a string. Minimal post-processing needed (e.g. strip route groups). Example: SvelteKit `route.id`. +- **`matched-records`** — Framework provides matched route records (array or tree). Iterate and concatenate path segments. Handle catch-all substitution. Example: Vue `to.matched[]`, React `state.matches[]`. +- **`param-substitution`** — Framework provides only the evaluated pathname + params object. Must reconstruct the route template by substituting values back with placeholders. Least preferred — heuristic and fragile. Example: Next.js `useParams()` + `usePathname()`. -- Vue: [lines 15-22 of vueRouter.ts](packages/rum-vue/src/domain/router/vueRouter.ts) -- React: subscribe callback in [createRouter.ts](packages/rum-react/src/domain/reactRouter/createRouter.ts) +### 4. Target Package (LLM Judgment) -**Test Strategy** -List every test case to implement, grouped by file: +Determine whether this router needs a new package or extends an existing one. -- `Plugin.spec.ts`: plugin structure, subscriber callbacks, telemetry, trackViewsManually -- `startView.spec.ts`: all view name computation cases (static, dynamic, nested, catch-all, edge cases) -- Router integration spec: navigation event handling, filtering, deduplication +- **`new-package`** — The router belongs to a framework with no existing SDK package (e.g., SvelteKit, Angular). Create `packages/rum-/`. +- **`extend-existing`** — The router is an alternative router for a framework that already has an SDK package (e.g., TanStack Router is a React router → extends `rum-react`). Add files under a subdirectory within the existing package. -**Trade-offs and Alternatives** -Document any decisions where multiple valid approaches existed. For each, state what was chosen, what was rejected, and why. +To decide: check if `packages/rum-*` already has a package for the same UI framework (React, Vue, etc.). If yes, extend it. If no, create new. -### Unmapped Concepts +For extend-existing, also determine the subdirectory path for the new router files (e.g., `src/domain/tanstackRouter/`). -For any framework concept that has no SDK equivalent, create a section: +### 5. Reference Implementation -```markdown -### Unmapped: +Select the `packages/rum-*` implementation that is closest across: -**Severity:** critical | minor -**Reason:** -**Impact:** -``` +- Hook subscription pattern +- Wrapping strategy +- Algorithm family -- `critical`: the integration cannot work without this (e.g. no way to get route matches) — **stop the pipeline** -- `minor`: the integration works but this feature isn't supported (e.g. named outlets not tracked) +Stage 3 reads this implementation as its primary model for code generation. -## Exit Criteria +### 6. SSR Handling (LLM Judgment) -If any unmapped concept has severity `critical`, stop the pipeline. Write an exit note at the top of the output explaining which concepts could not be mapped and why. +If `ssr.supported: true` in `01-router-concepts.json`, describe how the integration should ensure client-side-only execution. Use the `clientDetection` API from Stage 1 if available. -## Output +## Output Schema -Write the result to `docs/integrations//02-design-decisions.md`. +Return the populated object as your final message. The pipeline invokes you with `--output-format json --json-schema output.schema.json`; the harness validates the object and writes the full CLI wrapper (with the object on `.structured_output`) to `docs/integrations//02-design-decisions.json`. Do not write files yourself. diff --git a/.claude/skills/router-design/output.schema.json b/.claude/skills/router-design/output.schema.json new file mode 100644 index 0000000000..e26b9f127a --- /dev/null +++ b/.claude/skills/router-design/output.schema.json @@ -0,0 +1,111 @@ +{ + "title": "DesignDecisions", + "description": "Explicit design decisions derived from Stage 1 router concepts and reference implementations, used by Stage 3 to generate code.", + "type": "object", + "additionalProperties": false, + "required": ["selectedHook", "wrappingStrategy", "viewNameAlgorithm", "targetPackage", "referenceImplementation", "ssr"], + "properties": { + "selectedHook": { + "type": "object", + "additionalProperties": false, + "required": ["name", "rationale"], + "properties": { + "name": { + "type": "string", + "description": "Hook name from 01-router-concepts.json, selected by the deterministic priority rules." + }, + "rationale": { + "type": "string", + "description": "Which rules each candidate passed/failed and why the selected hook won." + } + } + }, + "wrappingStrategy": { + "type": "object", + "additionalProperties": false, + "required": ["pattern", "target", "rationale"], + "properties": { + "pattern": { + "enum": ["wrap-factory", "renderless-component", "provider", "wrap-hook", "other"], + "description": "wrap-factory: Wrap the router creation function, subscribe to hook inside. renderless-component: Component that calls the hook during lifecycle. provider: DI provider that injects the router and subscribes to events. wrap-hook: Wrap a user-facing hook to intercept route data. other: Escape hatch for unknown patterns." + }, + "target": { + "type": "string", + "description": "What specifically to wrap/provide. E.g. 'createRouter from vue-router', 'ENVIRONMENT_INITIALIZER with inject(Router)'." + }, + "rationale": { + "type": "string", + "description": "Why this is idiomatic for the framework." + } + } + }, + "viewNameAlgorithm": { + "type": "object", + "additionalProperties": false, + "required": ["family", "rationale"], + "properties": { + "family": { + "enum": ["route-id", "matched-records", "param-substitution"], + "description": "route-id: Framework provides parameterized route pattern as string. Minimal post-processing. matched-records: Framework provides matched route records. Iterate and concatenate path segments. param-substitution: Framework provides evaluated pathname + params. Reconstruct route template. Least preferred." + }, + "rationale": { + "type": "string", + "description": "Why this family, based on the hook's availableApi." + } + } + }, + "targetPackage": { + "type": "object", + "additionalProperties": false, + "required": ["mode", "package"], + "properties": { + "mode": { + "enum": ["new-package", "extend-existing"], + "description": "new-package: Create a new packages/rum-/ from scratch. extend-existing: Add router support to an existing package (e.g. adding TanStack Router to rum-react)." + }, + "package": { + "type": "string", + "description": "Target package directory name. For new-package: 'rum-'. For extend-existing: the existing package (e.g. 'rum-react')." + }, + "subpath": { + "type": "string", + "description": "Only for extend-existing. The subdirectory for this router's files within the existing package. E.g. 'src/domain/tanstackRouter/' within packages/rum-react/." + } + } + }, + "referenceImplementation": { + "type": "object", + "additionalProperties": false, + "required": ["primary", "rationale"], + "properties": { + "primary": { + "type": "string", + "description": "Which packages/rum-* to model after (e.g. 'rum-vue'). Stage 3 reads this implementation as its primary source for code patterns." + }, + "rationale": { + "type": "string", + "description": "Why this is the closest match." + } + } + }, + "ssr": { + "type": "object", + "additionalProperties": false, + "required": ["handling"], + "properties": { + "handling": { + "type": "string", + "description": "How to ensure client-side-only execution. 'N/A' if ssr.supported is false in Stage 1." + } + } + }, + "notes": { + "type": "string", + "description": "Free text for additional design context, trade-offs, unmapped concepts, or anything Stage 3 needs to know." + }, + "exitReason": { + "type": "string", + "description": "Only set if Stage 2 cannot proceed (e.g., no hook satisfies afterCancellation: true). When set, all other fields may be empty stubs and the pipeline will stop." + } + } +} diff --git a/.claude/skills/router-experiment/SKILL.md b/.claude/skills/router-experiment/SKILL.md new file mode 100644 index 0000000000..ce0952c66d --- /dev/null +++ b/.claude/skills/router-experiment/SKILL.md @@ -0,0 +1,140 @@ +--- +name: router-experiment +description: Run the router pipeline N times in parallel worktrees for the same framework to measure output consistency. Usage: /router-experiment [runs=3] +--- + +# Router Pipeline Consistency Experiment + +You run the router pipeline multiple times in parallel (each in its own git worktree) for the same npm package, then diff the outputs to measure consistency. Each run creates its own draft PR with a unique branch suffix. + +## Input + +- **Arg 1** (required): npm package URL (e.g. `https://www.npmjs.com/package/vue-router`) +- **Arg 2** (optional): number of parallel runs (default: 3) + +## Step 1: Setup + +```bash +RUNS= # default 3 +EXPERIMENT_DIR="/tmp/router-experiment-$(date +%s)" +mkdir -p "$EXPERIMENT_DIR" +``` + +Create worktrees from main: + +```bash +for i in $(seq 1 $RUNS); do + git worktree add "$EXPERIMENT_DIR/run-$i" main +done +``` + +## Step 2: Launch Parallel Runs + +Each run invokes `/router-pipeline` (stages 1–4) in its own worktree. Branch collisions are handled inside `/router-pr`, which appends a random suffix to the branch name. + +```bash +NPM_URL="" + +for i in $(seq 1 $RUNS); do + ( + cd "$EXPERIMENT_DIR/run-$i" + claude -p "/router-pipeline $NPM_URL" \ + --model opus \ + --allowedTools "Skill,Read,Write,Edit,Glob,Grep,Bash,WebFetch,WebSearch,Agent" + ) > "$EXPERIMENT_DIR/run-$i.log" 2>&1 & +done + +wait +``` + +Run via Bash with a generous timeout (up to 10 minutes). Use `run_in_background: true` so you can monitor progress. + +## Step 3: Compare Outputs + +### 3a. Discover framework name + +```bash +FRAMEWORK=$(basename $(dirname $(ls "$EXPERIMENT_DIR"/run-1/docs/integrations/*/01-router-concepts.json))) +``` + +### 3b. Diff artifacts + +The stage 1/2 artifacts are full `claude -p` wrappers — they include `duration_ms`, `session_id`, `total_cost_usd`, etc. that vary run-to-run. Diff the `structured_output` only, normalized with `jq -S`: + +```bash +for artifact in 01-router-concepts.json 02-design-decisions.json; do + echo "=== $artifact (structured_output) ===" + for i in $(seq 2 $RUNS); do + echo "--- run-1 vs run-$i ---" + diff -u \ + <(jq -S '.structured_output' "$EXPERIMENT_DIR/run-1/docs/integrations/$FRAMEWORK/$artifact") \ + <(jq -S '.structured_output' "$EXPERIMENT_DIR/run-$i/docs/integrations/$FRAMEWORK/$artifact") || true + done +done + +# Cost / duration / turns side-by-side +echo "=== cost & duration ===" +for i in $(seq 1 $RUNS); do + for artifact in 01-router-concepts.json 02-design-decisions.json; do + jq -r --arg run "run-$i" --arg art "$artifact" \ + '[$run, $art, .duration_ms, .num_turns, .total_cost_usd] | @tsv' \ + "$EXPERIMENT_DIR/run-$i/docs/integrations/$FRAMEWORK/$artifact" + done +done | column -t + +echo "=== 03-generation-manifest.md ===" +for i in $(seq 2 $RUNS); do + echo "--- run-1 vs run-$i ---" + diff -u "$EXPERIMENT_DIR/run-1/docs/integrations/$FRAMEWORK/03-generation-manifest.md" \ + "$EXPERIMENT_DIR/run-$i/docs/integrations/$FRAMEWORK/03-generation-manifest.md" || true +done +``` + +### 3c. Diff generated source code + +```bash +echo "=== Source code ===" +for i in $(seq 2 $RUNS); do + echo "--- run-1 vs run-$i ---" + diff -rq "$EXPERIMENT_DIR/run-1/packages/" "$EXPERIMENT_DIR/run-$i/packages/" || true +done +``` + +For files that differ, show the actual diff: + +```bash +for i in $(seq 2 $RUNS); do + diff -ru "$EXPERIMENT_DIR/run-1/packages/" "$EXPERIMENT_DIR/run-$i/packages/" || true +done +``` + +## Step 4: Report + +Present a summary table: + +``` +## Experiment Results: (N=) + +| Artifact | Identical? | Diff lines | +|---------------------------|------------|------------| +| 01-router-concepts.json | ✅ / ❌ | | +| 02-design-decisions.json | ✅ / ❌ | | +| 03-generation-manifest.md | ✅ / ❌ | | +| Generated source code | ✅ / ❌ | | + +### Observations + +``` + +If any diffs exist, show the most interesting ones inline (truncated if large). + +## Step 5: Cleanup + +Remove the worktrees: + +```bash +for i in $(seq 1 $RUNS); do + git worktree remove "$EXPERIMENT_DIR/run-$i" --force +done +rm -rf "$EXPERIMENT_DIR" +``` diff --git a/.claude/skills/router-fetch-docs/SKILL.md b/.claude/skills/router-fetch-docs/SKILL.md index 7f86ed8bcc..7a1a11c44f 100644 --- a/.claude/skills/router-fetch-docs/SKILL.md +++ b/.claude/skills/router-fetch-docs/SKILL.md @@ -1,119 +1,79 @@ --- name: router-fetch-docs -description: 'Stage 1: Fetch framework router documentation and extract structured routing concepts. Reads input from docs/integrations//00-pipeline-input.md.' +description: 'Stage 1: Fetch framework router documentation and extract structured routing concepts as JSON conforming to output.schema.json from an npm package URL.' --- # Stage 1: Fetch Router Documentation ## Context -You are Stage 1 of the router integration pipeline. Your job is to fetch framework router documentation and extract structured routing concepts as factual reference material for later stages. +You are Stage 1 of the router integration pipeline. Your job is to resolve package metadata, fetch framework router documentation, and extract structured routing concepts as **factual data** for later stages. + +The pipeline invokes you with `claude -p --output-format json --json-schema .claude/skills/router-fetch-docs/output.schema.json`. Your final message must be a single JSON object conforming to that schema; the harness validates it and exposes it on `.structured_output` of the CLI wrapper written to `docs/integrations//01-router-concepts.json`. Do NOT analyze which hooks the SDK should use, recommend approaches, or compare with existing SDK integrations. Only document what the framework provides. ## Input -Read `docs/integrations//00-pipeline-input.md` to get the framework name and documentation URLs. Find the `` directory by listing `docs/integrations/` and finding the most recently created subdirectory. +You receive an **npm package URL** as skill param (e.g. `https://www.npmjs.com/package/vue-router`). ## Process -### 1. Fetch Documentation - -Before fetching the provided URLs directly, try to find LLM-friendly versions of the docs: - -1. **Check for `llms.txt`** — Fetch `/llms.txt` (e.g. `https://svelte.dev/llms.txt`). This file indexes markdown documentation pages designed for LLM consumption. If it exists, use it to navigate to the relevant routing pages. -2. **Try `.md` suffix** — For each doc URL, try appending `.md` to the path (e.g. `https://svelte.dev/docs/kit/routing.md`). Many doc sites serve a raw markdown version this way, which is much easier to parse accurately. -3. **Fall back to HTML** — If neither LLM-friendly format is available, fetch the original URLs. - -Use the WebFetch tool for all fetches. If a URL fails, note it and continue with remaining URLs. If all URLs fail, write EXIT.md and stop. - -**Prefer over-fetching to under-fetching.** Fetch every routing-related page you can find — API references, guides, tutorials. It is better to fetch a page and not need it than to miss information that a later stage requires. When in doubt, fetch it. - -### 2. Extract Router Concepts - -Analyze the fetched documentation and produce a structured summary covering these sections. - -#### Sourcing Rules - -- Every API name (class, interface, function, event, type) MUST be a clickable link to its API reference page (e.g. `[GuardsCheckEnd](https://angular.dev/api/router/GuardsCheckEnd)`). -- Do NOT use generic `([source](url))` links. Instead, make the API name itself the link. -- Every factual claim MUST be linked — either via an API link on the relevant name, or via a `([guide](url))` / `([commit](url))` link for non-API content. -- Unsourced claims must be marked as _inferred: \_. +### 1. Resolve Package Metadata -#### Required Sections +Use WebFetch on the provided npm URL to extract: -**Route Definition Format** -How routes are declared: config object, file-based routing, decorators, or other. Include code examples from the docs. +- **Package name** (e.g. `vue-router`, `@angular/router`) +- **Framework identifier** — derive a lowercase identifier from the package name (e.g. `vue-router` → `vue`, `@angular/router` → `angular`, `@tanstack/react-router` → `tanstack-react-router`) +- Homepage / repository URL +- Keywords and description -**Dynamic Segment Syntax** -What syntax the framework uses for parameterized route segments (e.g. `:id`, `[id]`, `{id}`). Include examples. +Then **find router documentation URLs** — from the npm page metadata (homepage, repository links), locate the framework's official routing documentation: -**Catch-All / Wildcard Syntax** -What syntax the framework uses for catch-all or wildcard routes (e.g. `*`, `**`, `[...slug]`, `/:pathMatch(.*)*`). Include examples. If the framework has no catch-all syntax, state that explicitly. +- Check the homepage URL for docs links +- Check the GitHub repository README for documentation links +- Look for `/docs/`, `/guide/`, `/routing` paths on the framework's site +- Collect 1-3 relevant documentation URLs focused on routing -**Navigation Lifecycle Hooks** -List every navigation lifecycle event/hook the framework exposes, in the order they fire. For each, describe: +The pipeline creates the artifact directory for you — do not run `mkdir` yourself. -- When it fires relative to other hooks -- What data is available at that point -- Whether it can cancel/redirect navigation +### 2. Fetch Documentation -**Navigation Lifecycle Timing** -Document the order of operations during a navigation. For each phase, state: - -- Where in the lifecycle do **redirects** resolve? -- Where do **data fetches** (loaders, resolvers) execute? -- Where does **component rendering** begin? -- Which hooks fire between each phase? - -Include a timeline or ordered list showing the sequence: redirect resolution → guard evaluation → data fetching → component rendering, mapped to the specific hooks/events from the previous section. - -**Route Matching Model** -How the framework matches URLs to routes: nested vs flat, layout routes, named outlets/slots, parallel routes. - -**Programmatic Navigation API** -How the router exposes current route state and navigation methods. What objects/hooks are available to read the current route, its params, and matched route records. - -### 3. Major Version History (Last 2 Years) +Before fetching the provided URLs directly, try to find LLM-friendly versions of the docs: -Fetch the framework router's release/changelog information to identify major versions released within the last 2 years (since April 2024). +1. **Check for `llms.txt`** — Fetch `/llms.txt` (e.g. `https://svelte.dev/llms.txt`). This file indexes markdown documentation pages designed for LLM consumption. If it exists, use it to navigate to the relevant routing pages. +2. **Try `.md` suffix** — For each doc URL, try appending `.md` to the path (e.g. `https://svelte.dev/docs/kit/routing.md`). Many doc sites serve a raw markdown version this way, which is much easier to parse accurately. +3. **Fall back to HTML** — If neither LLM-friendly format is available, fetch the original URLs. -**How to find versions:** +Use the WebFetch tool for all fetches. If a URL fails, note it and continue with remaining URLs. If all URLs fail, stop without emitting a JSON object — the harness will mark the run as an error. -Use GitHub Releases to identify major versions. Fetch `https://github.com///releases` and filter to major versions (semver X.0.0) released after April 2024. For each major version found, fetch its individual release page to get the full release notes and breaking changes. +**Prefer over-fetching to under-fetching.** Fetch every routing-related page you can find — API references, guides, tutorials. It is better to fetch a page and not need it than to miss information that a later stage requires. When in doubt, fetch it. -**For each major version, document:** +### 3. Extract Router Concepts into the JSON Schema -- **Version number and release date** -- **Breaking changes** — list every breaking change from the release notes. Quote or link to the source. Do not filter, assess, or editorialize — just list them verbatim. +Analyze the fetched documentation and populate every field in the JSON schema. Use `null` for features the framework does not support. -**Output format:** +#### Sourcing Rules -```markdown -## Major Versions (Last 2 Years) +Every leaf field has a sibling `source` field. This is **mandatory** — the schema validation fails without it. -### vX.0.0 (YYYY-MM-DD) +- If extracted from documentation: set `source` to the URL (with anchor if possible) +- If inferred from multiple sources or reasoning: set `source` to `"inferred: "` +- API names, hook names, type names — everything factual must be traceable to a specific doc page -**Breaking Changes:** -- Change description ([source](url)) -- ... -``` +#### JSON Schema -If no major versions were released in the last 2 years, state that explicitly. +Read `output.schema.json` (next to this SKILL.md) for the schema and field descriptions. ### 4. Compatibility Assessment -At the end of the document, include a `## Compatibility` section with: - -- `compatible: true` or `compatible: false` -- If false, a `reason:` field explaining why - A framework is **incompatible** if it lacks: - A client-side route tree or route matching mechanism - Dynamic segment parameters - Navigation lifecycle events that can be hooked into -- Examples: Shopify Hydrogen (server-only loaders), Salesforce Lightning (proprietary component model) + +If incompatible: stop without emitting a JSON object. The harness will mark the run as an error. A framework is **compatible** even if: @@ -122,4 +82,4 @@ A framework is **compatible** even if: ## Output -Write the result to `docs/integrations//01-router-concepts.md`. +Return the populated object as your final message. The pipeline invokes you with `--output-format json --json-schema output.schema.json`; the harness validates the object and writes the full CLI wrapper (with the object on `.structured_output`) to `docs/integrations//01-router-concepts.json`. Do not write files yourself. diff --git a/.claude/skills/router-fetch-docs/output.schema.json b/.claude/skills/router-fetch-docs/output.schema.json new file mode 100644 index 0000000000..66403f7167 --- /dev/null +++ b/.claude/skills/router-fetch-docs/output.schema.json @@ -0,0 +1,224 @@ +{ + "title": "RouterConcepts", + "description": "Structured routing concepts extracted from framework documentation. Every factual leaf has a sibling `source` field: a URL (with anchor if possible) or 'inferred: '.", + "type": "object", + "additionalProperties": false, + "required": ["metadata", "routeDefinition", "routeSyntax", "ssr", "hooks", "versions"], + "properties": { + "metadata": { + "type": "object", + "additionalProperties": false, + "required": ["framework", "npmPackage"], + "properties": { + "framework": { + "type": "object", + "additionalProperties": false, + "required": ["value", "source"], + "properties": { + "value": { + "type": "string", + "description": "Lowercase identifier used for directory and package names. Derived from npm package name (e.g. vue-router → vue, @tanstack/react-router → tanstack-react-router)." + }, + "source": { "type": "string" } + } + }, + "npmPackage": { + "type": "object", + "additionalProperties": false, + "required": ["value", "source"], + "properties": { + "value": { + "type": "string", + "description": "npm package name (e.g. vue-router, @angular/router)." + }, + "source": { "type": "string" } + } + } + } + }, + "routeDefinition": { + "type": "object", + "additionalProperties": false, + "required": ["styles", "historyStrategies"], + "properties": { + "styles": { + "type": "array", + "description": "How routes are declared. file-based = filesystem convention (SvelteKit, Next.js). config-object = explicit route configuration array (Vue, React, Angular). other = escape hatch. Array because a framework may support multiple styles.", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["value", "source"], + "properties": { + "value": { "enum": ["file-based", "config-object", "other"] }, + "source": { "type": "string" } + } + } + }, + "historyStrategies": { + "type": "array", + "description": "URL strategies the router supports. path = browser History API (pushState). hash = hash-based (#/route). memory = in-memory (SSR, testing).", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["value", "source"], + "properties": { + "value": { "enum": ["path", "hash", "memory"] }, + "source": { "type": "string" } + } + } + } + } + }, + "routeSyntax": { + "type": "object", + "additionalProperties": false, + "required": ["concepts", "examples"], + "properties": { + "concepts": { + "type": "array", + "description": "Which routing concepts the framework supports. Use this enum to flag concepts so later stages never forget to handle them.", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["value", "source"], + "properties": { + "value": { "enum": ["static-path", "dynamic-segments", "optional-segments", "catch-all", "nested-routes"] }, + "source": { "type": "string" } + } + } + }, + "examples": { + "type": "array", + "description": "Behavioral examples showing route pattern, actual URL, and expected view name. Must cover every concept listed above. Include framework-specific quirks.", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["route", "path", "viewName", "source"], + "properties": { + "route": { "type": "string", "description": "Route pattern as declared." }, + "path": { "type": "string", "description": "Example URL that matches." }, + "viewName": { "type": "string", "description": "Expected view name (pattern with placeholders)." }, + "note": { "type": "string", "description": "Explain non-obvious behavior." }, + "source": { "type": "string" } + } + } + } + } + }, + "ssr": { + "type": "object", + "additionalProperties": false, + "required": ["supported", "clientDetection"], + "properties": { + "supported": { + "type": "object", + "additionalProperties": false, + "required": ["value", "source"], + "properties": { + "value": { "type": "boolean" }, + "source": { "type": "string" } + } + }, + "clientDetection": { + "type": "object", + "additionalProperties": false, + "required": ["value", "source"], + "properties": { + "value": { + "type": ["string", "null"], + "description": "Framework's idiomatic API for detecting client-side execution. Used by Stage 3 to generate guards when needed." + }, + "source": { "type": "string" } + } + } + } + }, + "hooks": { + "type": "array", + "description": "ALL candidate navigation lifecycle hooks (client-side).", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["name", "afterCancellation", "afterRedirects", "afterFetch", "afterRender", "access", "availableApi", "source"], + "properties": { + "name": { + "type": "string", + "description": "Hook/event name as it appears in the framework API." + }, + "afterCancellation": { + "type": "boolean", + "description": "At this hook, is the navigation guaranteed to have survived all cancellation points? Guard redirects (e.g. Angular returning a UrlTree) cancel the current navigation and start a new one — the hook fires only for the final, non-cancelled navigation." + }, + "afterRedirects": { + "type": "boolean", + "description": "At this hook, is the observed route guaranteed to be the final destination? Config redirects resolved before this hook, and programmatic redirects that cancel+restart navigation, both count as 'after' if the cancelled navigation never reaches this hook." + }, + "afterFetch": { + "type": "boolean", + "description": "Fires after data loading (resolvers, load functions)." + }, + "afterRender": { + "type": "boolean", + "description": "Fires after DOM mutation / component rendering." + }, + "access": { + "type": "string", + "description": "What runtime context is needed to call this hook. Examples: 'Return value of createRouter()', 'Must be called during Svelte component init', 'Router service injected via Angular DI'." + }, + "availableApi": { + "type": "array", + "description": "Data accessible at hook invocation time.", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["name", "description", "source"], + "properties": { + "name": { "type": "string", "description": "API path (e.g. to.matched, navigation.to.route.id)." }, + "description": { "type": "string" }, + "source": { "type": "string" } + } + } + }, + "source": { + "type": "string", + "description": "URL of the hook's API reference (or 'inferred: ')." + } + } + } + }, + "versions": { + "type": "object", + "additionalProperties": false, + "required": ["entries"], + "properties": { + "entries": { + "type": "array", + "description": "Routing-relevant breaking changes only, max 2 years old.", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["version", "date", "routingChanges", "source"], + "properties": { + "version": { "type": "string", "description": "Semver version string." }, + "date": { "type": "string", "description": "Release date (YYYY-MM-DD)." }, + "routingChanges": { + "type": "array", + "description": "Breaking changes affecting routing.", + "items": { "type": "string" } + }, + "source": { "type": "string", "description": "URL of changelog or release notes." } + } + } + } + } + }, + "notes": { + "type": "string", + "description": "Free text for framework-specific context that doesn't fit the schema above." + }, + "incompatibilityReason": { + "type": "string", + "description": "Only set if the framework is incompatible (no client-side route tree, no dynamic segments, or no navigation lifecycle hooks). When set, all other fields may be empty stubs and the pipeline will stop." + } + } +} diff --git a/.claude/skills/router-generate/SKILL.md b/.claude/skills/router-generate/SKILL.md index 14396b0666..5d7fcf927f 100644 --- a/.claude/skills/router-generate/SKILL.md +++ b/.claude/skills/router-generate/SKILL.md @@ -11,88 +11,172 @@ You are Stage 3 of the router integration pipeline. Your job is to generate a co ## Input -1. Read `docs/integrations//01-router-concepts.md` -2. Read `docs/integrations//02-design-decisions.md` — this is your primary guide -3. Read reference implementations for code patterns (read the actual source files, not summaries): - - The reference implementation identified as "closest" in the design doc - - Plugin: e.g. `packages/rum-vue/src/domain/vuePlugin.ts` - - Router: e.g. `packages/rum-vue/src/domain/router/` - - Tests: corresponding `.spec.ts` files - - Test helper: e.g. `packages/rum-vue/src/test/initializeVuePlugin.ts` - - Package config: e.g. `packages/rum-vue/package.json`, `packages/rum-vue/tsconfig.json` - - Entry point: e.g. `packages/rum-vue/src/entries/main.ts` +You receive a **framework identifier** as skill param (e.g. `angular`, `vue`, `tanstack-react-router`). + +Read: -Find the `` directory by listing `docs/integrations/`. +1. `docs/integrations//01-router-concepts.json` — stage 1 CLI wrapper. Extract the router-concepts payload with `jq '.structured_output' `. +2. `docs/integrations//02-design-decisions.json` — stage 2 CLI wrapper. Extract the design payload with `jq '.structured_output' `. +3. `.claude/skills/router-fetch-docs/output.schema.json` — field definitions for stage 1 output. +4. `.claude/skills/router-design/output.schema.json` — field definitions for stage 2 output. +5. Reference implementation specified by `referenceImplementation.primary` in the stage 2 payload. Read the source files in `packages//`: + - Plugin file: `src/domain/*Plugin.ts` + - Router files: `src/domain/*Router/` or `src/domain/router/` (all `.ts` files) + - Tests: corresponding `.spec.ts` files + - Test helper: `src/test/initialize*Plugin.ts` + - Package config: `package.json`, `tsconfig.json` + - Entry point: `src/entries/main.ts` ## Process -### 1. Generate Each File +Generate files in two phases. Complete each phase before starting the next. + +--- + +### Phase 1: Plugin + Wrapping Strategy + +Generate the plugin file and the framework-specific integration point. This phase reads `targetPackage`, `wrappingStrategy`, and `selectedHook` from `02-design-decisions.json`. + +**Check `targetPackage.mode` first:** + +- **`new-package`**: Create the full package scaffolding (`package.json`, `tsconfig.json`, `README.md`, `src/entries/main.ts`, `src/test/initializePlugin.ts`) by reading from the reference implementation. Then generate plugin + integration files. +- **`extend-existing`**: The package already exists at `packages//`. Do NOT create package scaffolding. Add router files under `targetPackage.subpath` (e.g., `src/domain/tanstackRouter/`). Update `src/entries/main.ts` to add new exports. Update `package.json` to add new peer dependencies if needed. The existing plugin file may need a new configuration option or the new router may reuse the existing plugin's `onRumInit`/`onRumStart` subscribers. + +**Files to generate:** -Follow the file structure defined in `02-design-decisions.md`. For each file: +For `new-package`: -1. Read the corresponding reference implementation file -2. Adapt it to the target framework using the decisions from `02-design-decisions.md` -3. Write the file using the Write tool +- `src/domain/Plugin.ts` — plugin lifecycle, subscriber pattern, configuration +- Integration point file — hook subscription +- `src/domain/Plugin.spec.ts` — plugin structure tests -**Code conventions** (from `AGENTS.md`): +For `extend-existing`: -- Use **camelCase** for all internal variables and object properties -- Conversion to snake_case happens at serialization boundary only -- Use TypeScript type narrowing over runtime assertions -- Follow existing patterns exactly — match import style, export style, comment style +- Integration point file under `targetPackage.subpath` — hook subscription +- View name and types files under `targetPackage.subpath` +- Tests under `targetPackage.subpath` +- Modify `src/entries/main.ts` — add exports for the new router +- Modify `package.json` — add peer dependencies if needed -**Plugin file** (`Plugin.ts`): +**Plugin file** (only for `new-package`): - Follow the exact pattern from the reference: global state, subscriber arrays, `onRumInit`/`onRumStart` exports, `resetPlugin` for tests - Plugin name must be the framework name in lowercase - Configuration interface with `router?: boolean` - `getConfigurationTelemetry` returning `{ router: !!configuration.router }` -**Router integration files**: +**Integration point file:** -- `types.ts`: minimal interface for route-related types, avoiding runtime framework imports where possible -- `startView.ts`: `computeViewName()` + `startRouterView()` calling `onRumInit` -- Integration point file: the framework-specific wrapper/provider/hook +The wrapping strategy from `02-design-decisions.json` determines what this file looks like: -**Test files**: +- `wrap-factory` → wrap the framework's router creation function, subscribe to hook inside +- `renderless-component` → Svelte/React component that calls the hook during lifecycle +- `provider` → DI provider that injects the router and subscribes to events +- `wrap-hook` → wrap a user-facing hook to intercept route data -- Follow Jasmine conventions: `describe`/`it` blocks -- Use `registerCleanupTask()` for cleanup, NOT `afterEach()` -- Use the test helper for plugin initialization -- Cover every test case listed in `02-design-decisions.md` +Read the selected hook's `access` and `availableApi` from `01-router-concepts.json` to understand what data is available and how to reach it. -**package.json**: +If `ssr.handling` is not "N/A", add the client-side guard described there. -- Follow the reference `package.json` structure exactly -- Set version to match current monorepo version (read from a reference package) -- Set correct peer dependencies from `02-design-decisions.md` -- Include both ESM and CJS entry points +--- + +### Phase 2: View Name Algorithm + +Generate the view name computation, types, and tests. This phase reads `viewNameAlgorithm` and `routeSyntax` from the design artifacts. + +**Files to generate:** + +- `src/domain/Router/startView.ts` — `computeViewName()` + `startRouterView()`. Driven by `viewNameAlgorithm.family`. +- `src/domain/Router/types.ts` — minimal local types for route data. Driven by `selectedHook.availableApi`. +- `src/domain/Router/startView.spec.ts` — view name computation tests. Driven by `routeSyntax.examples`. -**tsconfig.json**: +**`computeViewName()` implementation by family:** -- Copy from nearest reference package, adjust paths +- **`route-id`**: Framework provides route pattern as string. Implementation is minimal — mostly cleanup (e.g., strip route groups). Model after the simplest reference. +- **`matched-records`**: Iterate matched route records, concatenate `.path` fields. Handle absolute vs relative paths. Substitute catch-all patterns with actual path segments. Model after `packages/rum-vue/src/domain/router/startVueRouterView.ts` or `packages/rum-react/src/domain/reactRouter/startReactRouterView.ts`. +- **`param-substitution`**: Reverse-engineer route template from pathname + params. Two-pass: catch-all arrays first, then string params. Model after `packages/rum-nextjs/src/domain/nextJSRouter/computeViewNameFromParams.ts`. -**README.md**: +**Test cases:** -- Follow the reference README structure -- Include setup instructions matching the public API from `02-design-decisions.md` +Read the reference implementation's test files first. Match the same coverage and writing style: -### 2. Write Generation Manifest +- Same `describe`/`it` structure and naming conventions +- Same test case table format (e.g., `const cases = [...]` with inline comments) +- Same helper patterns (mock setup, cleanup) +- Same level of edge case coverage -After all files are generated, write `docs/integrations//03-generation-manifest.md`: +Then adapt test cases for the target framework: + +- Start from `routeSyntax.examples` in `01-router-concepts.json` — each example maps to a test case +- Add edge cases by looking at what the reference test covers and translating to the target framework's syntax +- Every `routeSyntax.concept` must have at least one test case + +**Types file:** + +- Define minimal interfaces matching what the selected hook's `availableApi` exposes +- Avoid runtime framework imports — use local type definitions +- Only type what `computeViewName()` and the integration point actually need + +--- -```markdown -# Generation Manifest +### Phase 3: Validate & Fix -## Generated Files +Run the monorepo toolchain against the generated code. Fix failures in a bounded loop. Only modify files you generated in Phase 1 and Phase 2 — never touch reference implementations, core packages, or config files outside the generated package. -| File | Purpose | Modeled After | Deviations | -| -------------------------------------------- | --------------------------- | ---------------------------------------------------------- | ---------- | -| `packages/rum-/src/domain/Plugin.ts` | Plugin + subscriber pattern | [`vuePlugin.ts`](packages/rum-vue/src/domain/vuePlugin.ts) | None | -| ... | ... | ... | ... | +**1. Register the package in the workspace:** + +```bash +yarn +``` + +For `new-package` mode, verify it appears in `yarn workspaces list`. + +**2. Run all checks, fix, repeat — up to 5 iterations:** + +``` +checks = [typecheck, lint, unit tests] + +for iteration in 1..5: + run `yarn format` + run `yarn typecheck` → capture errors + run `yarn lint` → capture errors + run `yarn test:unit --spec packages/rum-/` → capture errors + + if all pass → done, break + + analyze ALL errors together + apply fixes to generated files only + log what was fixed and why + +if iteration 5 still has failures → stop, document in manifest ``` -For each file, link to the reference file it was modeled after. If there are deviations from the reference pattern, explain why. +**Checks (run all three every iteration):** + +| Command | What it catches | +| ------------------------------------------------- | -------------------------------------------------------- | +| `yarn typecheck` | Wrong imports, missing properties, mismatched interfaces | +| `yarn lint` | Naming, formatting (mostly auto-fixed by `yarn format`) | +| `yarn test:unit --spec packages/rum-/` | Wrong mock shapes, assertion mismatches, missing cleanup | + +Running all three every iteration (instead of gating sequentially) lets you see the full error surface and fix multiple categories at once. + +**Fix strategy:** + +- Read ALL errors before making any changes — fixes often overlap +- Compare with the reference implementation's source and test files to spot pattern mismatches +- Prefer the smallest change that fixes the error + +**Anti-rules — never do these to "fix" a failure:** + +- Add `// @ts-ignore`, `as any`, or type casts to silence type errors +- Delete or skip failing test cases + +## After All Phases: Write Generation Manifest + +Write `docs/integrations//03-generation-manifest.md` using the template at `output.template.md` (next to this SKILL.md). Fill in every placeholder. The validation section documents all fix loop iterations. + +If validation status is `fail`, the pipeline orchestrator must stop before creating a PR. ## Output diff --git a/.claude/skills/router-generate/output.template.md b/.claude/skills/router-generate/output.template.md new file mode 100644 index 0000000000..b9a607787b --- /dev/null +++ b/.claude/skills/router-generate/output.template.md @@ -0,0 +1,27 @@ +# Generation Manifest — + +## Generated Files + +### Phase 1: Plugin + Wrapping Strategy + +- `` — + - Reference: `` + - Deviations: + +### Phase 2: View Name Algorithm + +- `` — + - Reference: `` + - Deviations: + +## Validation + +**Status:** pass | fail +**Iterations:** / 5 + +### Iteration , , + +- + + + diff --git a/.claude/skills/router-pipeline/SKILL.md b/.claude/skills/router-pipeline/SKILL.md index 0556c58936..e44c80426c 100644 --- a/.claude/skills/router-pipeline/SKILL.md +++ b/.claude/skills/router-pipeline/SKILL.md @@ -5,7 +5,9 @@ description: Fully automated pipeline that generates a draft Browser SDK router # Router Integration Pipeline -You are an orchestrator that chains four stage skills to generate a complete Browser SDK router integration package and draft PR. +You are an orchestrator that dispatches `claude -p` processes for each stage to generate a complete Browser SDK router integration package and draft PR. + +Each stage runs as a dedicated `claude -p` process with its own context window. Stages 1 and 2 use `--output-format json --json-schema `. The full CLI wrapper JSON (including `structured_output`, `is_error`, `duration_ms`, cost, etc.) is persisted as the reviewable artifact under `docs/integrations//`. Downstream stages extract `.structured_output` with `jq` when they read the data. Stages 3 and 4 produce code and markdown via normal tool calls. ## Input @@ -13,83 +15,64 @@ The single argument is an npm package URL (e.g. `https://www.npmjs.com/package/@ Example: `/router-pipeline https://www.npmjs.com/package/vue-router` -## Step 1: Resolve Package Metadata & Initialize - -1. **Fetch npm package page** — Use WebFetch on the provided URL to extract: - - Package name (e.g. `vue-router`, `@angular/router`) - - Framework name — derive a lowercase identifier from the package name (e.g. `vue-router` → `vue`, `@angular/router` → `angular`, `@tanstack/react-router` → `tanstack-react-router`, `svelte` → `svelte`) - - Homepage / repository URL - - Keywords and description +## Step 1: Stage 1 — Fetch Docs (structured JSON) -2. **Find router documentation URLs** — From the npm page metadata (homepage, repository links), locate the framework's official routing documentation: - - Check the homepage URL for docs links - - Check the GitHub repository README for documentation links - - Look for `/docs/`, `/guide/`, `/routing` paths on the framework's site - - Collect 1-3 relevant documentation URLs focused on routing +The skill instructs the model to emit a JSON object conforming to `.claude/skills/router-fetch-docs/output.schema.json`. The harness validates it and exposes it on `.structured_output`. -3. **Create artifact directory and input file:** +The framework name isn't known yet, so write the artifact to a real temp file first, then move it under `docs/integrations//` once stage 1 resolves it. ```bash -mkdir -p docs/integrations/ -``` - -Write `docs/integrations//00-pipeline-input.md`: - -```markdown -# Pipeline Input +SCHEMA_1=$(cat .claude/skills/router-fetch-docs/output.schema.json) -**Framework:** -**npm package:** -**Documentation URLs:** +STAGE1_OUT=/tmp/router-stage1.json -- -- +claude -p "/router-fetch-docs " \ + --model opus \ + --output-format json \ + --json-schema "$SCHEMA_1" \ + --permission-mode auto \ + > "$STAGE1_OUT" -**Initiated:** +FRAMEWORK=$(jq -r '.structured_output.metadata.framework.value' "$STAGE1_OUT") +mkdir -p "docs/integrations/$FRAMEWORK" +mv "$STAGE1_OUT" "docs/integrations/$FRAMEWORK/01-router-concepts.json" ``` -## Step 2: Invoke /router-fetch-docs - -Use the Skill tool to invoke `router-fetch-docs`. - -After completion, read `docs/integrations//01-router-concepts.md` and check the `compatible` field. - -If `compatible: false`, write `docs/integrations//EXIT.md` with: - -- Stage: fetch-docs -- Reason: the incompatibility reason from 01-router-concepts.md -- Artifacts produced: 00-pipeline-input.md, 01-router-concepts.md - -Then stop and report the exit to the user. +The file committed under `docs/integrations/$FRAMEWORK/01-router-concepts.json` is the full CLI wrapper: `{type, subtype, is_error, duration_ms, result, structured_output, usage, total_cost_usd, ...}`. Downstream stages use `jq '.structured_output' ` to read the schema-shaped payload. -## Step 3: Invoke /router-design +## Step 2: Stage 2 — Design Decisions (structured JSON) -Use the Skill tool to invoke `router-design`. - -After completion, read `docs/integrations//02-design-decisions.md` and check for any concept marked `unmapped` with severity `critical`. - -If critical unmapped concepts exist, write `EXIT.md` with: - -- Stage: design -- Reason: which critical concepts could not be mapped and why -- Artifacts produced: 00-pipeline-input.md, 01-router-concepts.md, 02-design-decisions.md - -Then stop and report the exit to the user. +```bash +SCHEMA_2=$(cat .claude/skills/router-design/output.schema.json) + +claude -p "/router-design $FRAMEWORK" \ + --model opus \ + --output-format json \ + --json-schema "$SCHEMA_2" \ + --permission-mode auto \ + > "docs/integrations/$FRAMEWORK/02-design-decisions.json" +``` -## Step 4: Invoke /router-generate +## Step 3: Stage 3 — Generate Code -Use the Skill tool to invoke `router-generate`. +Stage 3 produces source code and a markdown manifest via normal tool calls — no structured output. -## Step 5: Invoke /router-pr +```bash +claude -p "/router-generate $FRAMEWORK" \ + --model opus \ + --permission-mode auto +``` -Use the Skill tool to invoke `router-pr`. +Verify `docs/integrations/$FRAMEWORK/03-generation-manifest.md` -## Error Handling +Check the **Validation** section in the manifest. If `**Status:** fail`, stop the pipeline and report the failures to the user. Do not proceed to Stage 4. -If any stage fails (fetch timeout, parse error, tool failure), write `EXIT.md` with: +## Step 4: Stage 4 — Create PR -- Stage: which stage failed -- Reason: error details -- Artifacts produced: list of files written before failure +```bash +claude -p "/router-pr $FRAMEWORK" \ + --model opus \ + --permission-mode auto +``` -All artifacts written before the failure are preserved. Stop and report the failure to the user. +Report the PR URL to the user when done. diff --git a/.claude/skills/router-pr/SKILL.md b/.claude/skills/router-pr/SKILL.md index 73040896a1..8bd597bc57 100644 --- a/.claude/skills/router-pr/SKILL.md +++ b/.claude/skills/router-pr/SKILL.md @@ -11,11 +11,13 @@ You are Stage 4 of the router integration pipeline. Your job is to create a bran ## Input -1. Read `docs/integrations//02-design-decisions.md` for PR body content -2. Read `docs/integrations//03-generation-manifest.md` for the list of generated files -3. Determine the current git user name from `git config user.name` (for branch naming) +You receive a **framework identifier** as skill param (e.g. `angular`, `vue`, `tanstack-react-router`). -Find the `` directory by listing `docs/integrations/`. +Read: + +1. `docs/integrations//02-design-decisions.json` — stage 2 CLI wrapper. Extract the design payload (used for PR body content) with `jq '.structured_output' `. +2. `docs/integrations//03-generation-manifest.md` — list of generated files. +3. `git config user.name` — current git user name (for branch naming). ## Process @@ -24,7 +26,8 @@ Find the `` directory by listing `docs/integrations/`. ```bash # Get the current user's branch prefix USER=$(git config user.name | tr ' ' '.' | tr '[:upper:]' '[:lower:]') -BRANCH="${USER}/-router-integration" +# Random suffix to avoid branch collisions across repeated runs +BRANCH="${USER}/-router-integration-${RANDOM}" git checkout -b "$BRANCH" ``` @@ -38,12 +41,14 @@ git add docs/integrations// git commit -m "📝 Add router integration design docs Generated by the router integration pipeline. -Artifacts: pipeline input, router concepts, design decisions, generation manifest." +Artifacts: router concepts, design decisions, generation manifest." ``` -### 3. Commit Generated Package +### 3. Commit Generated Code + +Second commit: the generated code. Read `targetPackage.mode` from `02-design-decisions.json`. -Second commit: the generated package. +For `new-package`: ```bash git add packages/rum-/ @@ -53,6 +58,16 @@ Auto-generated from framework documentation using the router integration pipelin See docs/integrations// for design artifacts and decision rationale." ``` +For `extend-existing`: + +```bash +git add packages// +git commit -m "✨ Add router support to + +Auto-generated from framework documentation using the router integration pipeline. +See docs/integrations// for design artifacts and decision rationale." +``` + ### 4. Push and Create Draft PR ```bash @@ -73,13 +88,13 @@ Auto-generated router integration for using the router integration p ## Design Artifacts Full design trail at `docs/integrations//`: -- `01-router-concepts.md` — extracted routing concepts from framework docs -- `02-design-decisions.md` — SDK mapping, architecture and API decisions +- `01-router-concepts.json` — structured routing concepts extracted from framework docs +- `02-design-decisions.json` — hook selection, wrapping strategy, algorithm family decisions - `03-generation-manifest.md` — list of generated files with lineage ## Key Decisions - + ## Test plan @@ -94,7 +109,7 @@ PREOF )" ``` -Replace `` with the actual framework name. Replace the key decisions placeholder with actual content from `02-design-decisions.md`. +Replace `` with the actual framework name. Replace the key decisions placeholder with actual content from `02-design-decisions.json`. ## Output From da3c6f120a32e9e3ac45850b86472638d499e9e7 Mon Sep 17 00:00:00 2001 From: Aymeric Mortemousque Date: Fri, 17 Apr 2026 14:23:59 +0200 Subject: [PATCH 10/12] =?UTF-8?q?=F0=9F=99=88=20ignore=20.worktrees/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index af4e781f89..0bf48db32e 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ playwright-report/ # Claude Code local files *.local.md .claude/settings.local.json +.worktrees/ From 5cedaf42e0e4203965174cb2619ad7255a0008d1 Mon Sep 17 00:00:00 2001 From: Aymeric Mortemousque Date: Fri, 17 Apr 2026 14:54:18 +0200 Subject: [PATCH 11/12] =?UTF-8?q?=F0=9F=93=9D=20Add=20angular=20router=20i?= =?UTF-8?q?ntegration=20design=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generated by the router integration pipeline. Artifacts: router concepts, design decisions, generation manifest. --- .../angular/01-router-concepts.json | 1 + .../angular/02-design-decisions.json | 1 + .../angular/03-generation-manifest.md | 87 +++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 docs/integrations/angular/01-router-concepts.json create mode 100644 docs/integrations/angular/02-design-decisions.json create mode 100644 docs/integrations/angular/03-generation-manifest.md diff --git a/docs/integrations/angular/01-router-concepts.json b/docs/integrations/angular/01-router-concepts.json new file mode 100644 index 0000000000..ba2be0305b --- /dev/null +++ b/docs/integrations/angular/01-router-concepts.json @@ -0,0 +1 @@ +{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":172092,"duration_api_ms":171024,"num_turns":14,"result":"Emitted structured output. Note: WebFetch was denied, so all sources are marked `inferred:` from training knowledge — Stage 2 should verify API shapes (event properties, NavigationCancellationCode/NavigationSkippedCode enums) against live angular.dev before generating code.","stop_reason":"end_turn","session_id":"f031446b-08f3-4f62-9106-51302fc67cdd","total_cost_usd":1.1494074999999997,"usage":{"input_tokens":29,"cache_creation_input_tokens":93296,"cache_read_input_tokens":493375,"output_tokens":12779,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":93296},"inference_geo":"","iterations":[{"input_tokens":1,"output_tokens":98,"cache_read_input_tokens":42778,"cache_creation_input_tokens":10708,"cache_creation":{"ephemeral_5m_input_tokens":10708,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-opus-4-7":{"inputTokens":29,"outputTokens":12779,"cacheReadInputTokens":493375,"cacheCreationInputTokens":93296,"webSearchRequests":0,"costUSD":1.1494074999999997,"contextWindow":200000,"maxOutputTokens":64000}},"permission_denials":[{"tool_name":"WebFetch","tool_use_id":"toolu_01MZbPVuHH4CKqcFLMFbKvXx","tool_input":{"url":"https://www.npmjs.com/package/@angular/router","prompt":"Extract: package name, version, description, homepage URL, repository URL, and all documentation links. Also look for keywords. Provide URLs exactly as they appear."}},{"tool_name":"WebFetch","tool_use_id":"toolu_017YkWNbmwMboqW3Gn2veQuR","tool_input":{"url":"https://www.npmjs.com/package/@angular/router","prompt":"Extract: package name, version, description, homepage URL, repository URL, and all documentation links. Also look for keywords. Provide URLs exactly as they appear."}},{"tool_name":"WebFetch","tool_use_id":"toolu_01Y2rRsxFbNB3uaZj1WuGacJ","tool_input":{"url":"https://www.npmjs.com/package/@angular/router","prompt":"Extract: package name, version, description, homepage URL, repository URL, and all documentation links. Also look for keywords. Provide URLs exactly as they appear."}},{"tool_name":"WebFetch","tool_use_id":"toolu_011LKMAEBmkZMZDEiGoSeAdv","tool_input":{"url":"https://www.npmjs.com/package/@angular/router","prompt":"Extract: package name, version, description, homepage URL, repository URL, and all documentation links. Also look for keywords. Provide URLs exactly as they appear."}},{"tool_name":"WebFetch","tool_use_id":"toolu_01BgYFY8J2yzgx23VYRrquwG","tool_input":{"url":"https://angular.dev/guide/routing","prompt":"List all the subpages and sections of this routing guide, and any links to API references. Also summarize what this page covers about client-side routing."}},{"tool_name":"WebFetch","tool_use_id":"toolu_0165R1zZJofcnYQ3ufzkJwqH","tool_input":{"url":"https://angular.dev/llms.txt","prompt":"Extract the full content of this llms.txt file and list all routing-related pages."}},{"tool_name":"WebFetch","tool_use_id":"toolu_01U3MWx2hWzkfwTKU3nzs4gD","tool_input":{"url":"https://angular.dev/llms.txt","prompt":"Extract the full content and list all routing-related pages."}},{"tool_name":"WebFetch","tool_use_id":"toolu_0151rzZUi5RTKiRXpiZVoiHX","tool_input":{"url":"https://www.npmjs.com/package/@angular/router","prompt":"Extract package name, version, description, homepage URL, repository URL, and keywords."}},{"tool_name":"WebFetch","tool_use_id":"toolu_01JnGc7iZZyH4H7eh4Vt5uRE","tool_input":{"url":"https://www.npmjs.com/package/@angular/router","prompt":"Extract package name, version, description, homepage URL, repository URL, and keywords."}}],"structured_output":{"metadata":{"framework":{"value":"angular","source":"inferred: derived from npm package @angular/router; WebFetch permission not granted"},"npmPackage":{"value":"@angular/router","source":"inferred: URL https://www.npmjs.com/package/@angular/router provided as skill argument; fetch denied"}},"routeDefinition":{"styles":[{"value":"config-object","source":"inferred: Angular Router uses Routes arrays with path/component/children/loadChildren; Routes API at https://angular.dev/api/router/Routes"}],"historyStrategies":[{"value":"path","source":"inferred: PathLocationStrategy is default (History API); https://angular.dev/api/common/PathLocationStrategy"},{"value":"hash","source":"inferred: HashLocationStrategy opt-in via withHashLocation() or {useHash:true}; https://angular.dev/api/common/HashLocationStrategy"}]},"routeSyntax":{"concepts":[{"value":"static-path","source":"inferred: Routes support literal paths like {path: 'users'}; Angular Router core API"},{"value":"dynamic-segments","source":"inferred: colon-prefixed segments like :id; https://angular.dev/guide/routing/common-router-tasks"},{"value":"catch-all","source":"inferred: wildcard ** matches any unmatched URL; https://angular.dev/guide/routing/common-router-tasks"},{"value":"nested-routes","source":"inferred: children property on Route produces nested routes rendered in child router-outlet"}],"examples":[{"route":"users","path":"/users","viewName":"UsersComponent","note":"Static path; matches exact segment.","source":"inferred: Angular Router standard usage"},{"route":"users/:id","path":"/users/42","viewName":"UserComponent (params.id='42')","note":"Dynamic segment; accessed via ActivatedRoute.paramMap or @Input() when withComponentInputBinding() enabled.","source":"inferred: Angular Router standard usage; https://angular.dev/guide/routing/common-router-tasks"},{"route":"**","path":"/anything/unmatched","viewName":"PageNotFoundComponent","note":"Wildcard route; must be declared last in Routes array.","source":"inferred: https://angular.dev/guide/routing/common-router-tasks"},{"route":"admin with children [{path:'settings', component: SettingsComponent}]","path":"/admin/settings","viewName":"AdminComponent > SettingsComponent","note":"Nested routes require a inside the parent component.","source":"inferred: https://angular.dev/guide/routing/common-router-tasks"},{"route":"products with children [{path:':id', component: ProductComponent}]","path":"/products/10","viewName":"ProductsComponent > ProductComponent","note":"Dynamic nested segment.","source":"inferred: Angular Router standard usage"}]},"ssr":{"supported":{"value":true,"source":"inferred: Angular SSR (formerly Universal) via @angular/ssr; https://angular.dev/guide/ssr"},"clientDetection":{"value":"isPlatformBrowser(inject(PLATFORM_ID))","source":"inferred: idiomatic SSR guard using @angular/common PLATFORM_ID token; https://angular.dev/api/common/isPlatformBrowser"}},"hooks":[{"name":"NavigationStart","afterCancellation":false,"afterRedirects":false,"afterFetch":false,"afterRender":false,"access":"Subscribe to Router.events (Router service injected via Angular DI); filter by instanceof NavigationStart","availableApi":[{"name":"event.id","description":"Unique navigation identifier (number).","source":"inferred: https://angular.dev/api/router/NavigationStart"},{"name":"event.url","description":"Target URL string (pre-redirect).","source":"inferred: https://angular.dev/api/router/NavigationStart"},{"name":"event.navigationTrigger","description":"'imperative' | 'popstate' | 'hashchange'.","source":"inferred: https://angular.dev/api/router/NavigationStart"},{"name":"event.restoredState","description":"Popstate state snapshot when navigation triggered by back/forward.","source":"inferred: https://angular.dev/api/router/NavigationStart"}],"source":"inferred: https://angular.dev/api/router/NavigationStart"},{"name":"RouteConfigLoadStart","afterCancellation":false,"afterRedirects":false,"afterFetch":false,"afterRender":false,"access":"Subscribe to Router.events; filter by instanceof RouteConfigLoadStart","availableApi":[{"name":"event.route","description":"The Route config about to be lazy-loaded.","source":"inferred: https://angular.dev/api/router/RouteConfigLoadStart"}],"source":"inferred: https://angular.dev/api/router/RouteConfigLoadStart"},{"name":"RouteConfigLoadEnd","afterCancellation":false,"afterRedirects":false,"afterFetch":false,"afterRender":false,"access":"Subscribe to Router.events; filter by instanceof RouteConfigLoadEnd","availableApi":[{"name":"event.route","description":"The Route config just lazy-loaded.","source":"inferred: https://angular.dev/api/router/RouteConfigLoadEnd"}],"source":"inferred: https://angular.dev/api/router/RouteConfigLoadEnd"},{"name":"RoutesRecognized","afterCancellation":false,"afterRedirects":true,"afterFetch":false,"afterRender":false,"access":"Subscribe to Router.events; filter by instanceof RoutesRecognized","availableApi":[{"name":"event.id","description":"Navigation id.","source":"inferred: https://angular.dev/api/router/RoutesRecognized"},{"name":"event.url","description":"Original URL.","source":"inferred: https://angular.dev/api/router/RoutesRecognized"},{"name":"event.urlAfterRedirects","description":"Final URL after configured redirects applied.","source":"inferred: https://angular.dev/api/router/RoutesRecognized"},{"name":"event.state","description":"RouterStateSnapshot with the matched route tree; walk state.root to access ActivatedRouteSnapshot nodes including routeConfig.path, params, data.","source":"inferred: https://angular.dev/api/router/RouterStateSnapshot"}],"source":"inferred: https://angular.dev/api/router/RoutesRecognized"},{"name":"GuardsCheckStart","afterCancellation":false,"afterRedirects":true,"afterFetch":false,"afterRender":false,"access":"Subscribe to Router.events; filter by instanceof GuardsCheckStart","availableApi":[{"name":"event.state","description":"RouterStateSnapshot prior to guards running.","source":"inferred: https://angular.dev/api/router/GuardsCheckStart"},{"name":"event.urlAfterRedirects","description":"Final URL after config redirects.","source":"inferred: https://angular.dev/api/router/GuardsCheckStart"}],"source":"inferred: https://angular.dev/api/router/GuardsCheckStart"},{"name":"GuardsCheckEnd","afterCancellation":false,"afterRedirects":true,"afterFetch":false,"afterRender":false,"access":"Subscribe to Router.events; filter by instanceof GuardsCheckEnd","availableApi":[{"name":"event.shouldActivate","description":"Boolean: whether guards permitted activation; false triggers NavigationCancel afterwards.","source":"inferred: https://angular.dev/api/router/GuardsCheckEnd"},{"name":"event.state","description":"RouterStateSnapshot.","source":"inferred: https://angular.dev/api/router/GuardsCheckEnd"}],"source":"inferred: https://angular.dev/api/router/GuardsCheckEnd"},{"name":"ResolveStart","afterCancellation":true,"afterRedirects":true,"afterFetch":false,"afterRender":false,"access":"Subscribe to Router.events; filter by instanceof ResolveStart","availableApi":[{"name":"event.state","description":"RouterStateSnapshot entering resolve phase.","source":"inferred: https://angular.dev/api/router/ResolveStart"}],"source":"inferred: https://angular.dev/api/router/ResolveStart"},{"name":"ResolveEnd","afterCancellation":true,"afterRedirects":true,"afterFetch":true,"afterRender":false,"access":"Subscribe to Router.events; filter by instanceof ResolveEnd","availableApi":[{"name":"event.state","description":"RouterStateSnapshot with resolved data on ActivatedRouteSnapshot.data.","source":"inferred: https://angular.dev/api/router/ResolveEnd"},{"name":"event.urlAfterRedirects","description":"Final URL.","source":"inferred: https://angular.dev/api/router/ResolveEnd"}],"source":"inferred: https://angular.dev/api/router/ResolveEnd"},{"name":"ActivationStart","afterCancellation":true,"afterRedirects":true,"afterFetch":true,"afterRender":false,"access":"Subscribe to Router.events; filter by instanceof ActivationStart","availableApi":[{"name":"event.snapshot","description":"ActivatedRouteSnapshot of the route being activated (one per activation).","source":"inferred: https://angular.dev/api/router/ActivationStart"}],"source":"inferred: https://angular.dev/api/router/ActivationStart"},{"name":"ActivationEnd","afterCancellation":true,"afterRedirects":true,"afterFetch":true,"afterRender":false,"access":"Subscribe to Router.events; filter by instanceof ActivationEnd","availableApi":[{"name":"event.snapshot","description":"ActivatedRouteSnapshot of activated route.","source":"inferred: https://angular.dev/api/router/ActivationEnd"}],"source":"inferred: https://angular.dev/api/router/ActivationEnd"},{"name":"ChildActivationStart","afterCancellation":true,"afterRedirects":true,"afterFetch":true,"afterRender":false,"access":"Subscribe to Router.events; filter by instanceof ChildActivationStart","availableApi":[{"name":"event.snapshot","description":"ActivatedRouteSnapshot of the parent whose children are activating.","source":"inferred: https://angular.dev/api/router/ChildActivationStart"}],"source":"inferred: https://angular.dev/api/router/ChildActivationStart"},{"name":"ChildActivationEnd","afterCancellation":true,"afterRedirects":true,"afterFetch":true,"afterRender":false,"access":"Subscribe to Router.events; filter by instanceof ChildActivationEnd","availableApi":[{"name":"event.snapshot","description":"ActivatedRouteSnapshot of the parent whose children activated.","source":"inferred: https://angular.dev/api/router/ChildActivationEnd"}],"source":"inferred: https://angular.dev/api/router/ChildActivationEnd"},{"name":"NavigationEnd","afterCancellation":true,"afterRedirects":true,"afterFetch":true,"afterRender":false,"access":"Subscribe to Router.events; filter by instanceof NavigationEnd (or use router.events.pipe(filter(e => e instanceof NavigationEnd)))","availableApi":[{"name":"event.id","description":"Navigation id.","source":"inferred: https://angular.dev/api/router/NavigationEnd"},{"name":"event.url","description":"Initial URL.","source":"inferred: https://angular.dev/api/router/NavigationEnd"},{"name":"event.urlAfterRedirects","description":"Final URL committed to the browser.","source":"inferred: https://angular.dev/api/router/NavigationEnd"},{"name":"router.routerState.snapshot","description":"RouterStateSnapshot of the activated route tree; accessible via Router service.","source":"inferred: https://angular.dev/api/router/Router"},{"name":"router.routerState.root","description":"ActivatedRoute root; traverse firstChild chain to reach leaf; each node exposes routeConfig.path and snapshot.params.","source":"inferred: https://angular.dev/api/router/ActivatedRoute"}],"source":"inferred: https://angular.dev/api/router/NavigationEnd"},{"name":"NavigationCancel","afterCancellation":false,"afterRedirects":false,"afterFetch":false,"afterRender":false,"access":"Subscribe to Router.events; filter by instanceof NavigationCancel","availableApi":[{"name":"event.reason","description":"String explanation of cancellation.","source":"inferred: https://angular.dev/api/router/NavigationCancel"},{"name":"event.code","description":"NavigationCancellationCode enum (Redirect, SupersededByNewNavigation, NoDataFromResolver, GuardRejected).","source":"inferred: https://angular.dev/api/router/NavigationCancellationCode"}],"source":"inferred: https://angular.dev/api/router/NavigationCancel"},{"name":"NavigationError","afterCancellation":false,"afterRedirects":false,"afterFetch":false,"afterRender":false,"access":"Subscribe to Router.events; filter by instanceof NavigationError","availableApi":[{"name":"event.error","description":"Error thrown during navigation.","source":"inferred: https://angular.dev/api/router/NavigationError"},{"name":"event.url","description":"URL that failed.","source":"inferred: https://angular.dev/api/router/NavigationError"}],"source":"inferred: https://angular.dev/api/router/NavigationError"},{"name":"NavigationSkipped","afterCancellation":true,"afterRedirects":false,"afterFetch":false,"afterRender":false,"access":"Subscribe to Router.events; filter by instanceof NavigationSkipped","availableApi":[{"name":"event.reason","description":"NavigationSkippedCode enum (IgnoredSameUrlNavigation, IgnoredByUrlHandlingStrategy).","source":"inferred: https://angular.dev/api/router/NavigationSkipped"},{"name":"event.url","description":"URL that was skipped.","source":"inferred: https://angular.dev/api/router/NavigationSkipped"}],"source":"inferred: https://angular.dev/api/router/NavigationSkipped"},{"name":"Scroll","afterCancellation":true,"afterRedirects":true,"afterFetch":true,"afterRender":true,"access":"Subscribe to Router.events; filter by instanceof Scroll (only fired when withInMemoryScrolling() feature enabled)","availableApi":[{"name":"event.routerEvent","description":"The originating NavigationEnd event.","source":"inferred: https://angular.dev/api/router/Scroll"},{"name":"event.position","description":"[x, y] scroll restore position, or null.","source":"inferred: https://angular.dev/api/router/Scroll"},{"name":"event.anchor","description":"URL fragment identifier, or null.","source":"inferred: https://angular.dev/api/router/Scroll"}],"source":"inferred: https://angular.dev/api/router/Scroll"}],"versions":{"entries":[{"version":"17.0.0","date":"2023-11-08","routingChanges":["Deprecated CanLoad in favor of CanMatch guard.","Added withComponentInputBinding() feature to bind route params/data/queryParams as component @Input.","redirectTo may be a function receiving ActivatedRouteSnapshot."],"source":"inferred: Angular v17 release notes (https://github.com/angular/angular/blob/main/CHANGELOG.md)"},{"version":"18.0.0","date":"2024-05-22","routingChanges":["Router events surface NavigationSkipped with refined NavigationSkippedCode enum.","withNavigationErrorHandler() router feature added for structured error handling."],"source":"inferred: Angular v18 release notes"},{"version":"19.0.0","date":"2024-11-19","routingChanges":["Standalone APIs by default (affects provideRouter usage patterns).","Event replay hydration integrated with router for SSR."],"source":"inferred: Angular v19 release notes"},{"version":"20.0.0","date":"2025-05-28","routingChanges":["Further tightening of typed route data/params; continued stabilization of signal-based inputs with router input binding."],"source":"inferred: Angular v20 release notes; date approximate from Angular's 6-month cadence"}]},"notes":"Documentation could not be fetched (WebFetch permission denied); all fields were populated from training knowledge of Angular Router and marked 'inferred'. Stage 2 should independently verify any API names (especially event property shapes and NavigationCancellationCode/NavigationSkippedCode enum members) against live angular.dev docs before generating code.\n\nKey Angular-specific nuances for downstream stages:\n- Router.events emits *all* event types on one Observable; SDK must filter by instanceof.\n- Route tree reconstruction: after NavigationEnd, walk router.routerState.root.firstChild recursively; each ActivatedRouteSnapshot has routeConfig?.path (literal pattern, may be '', ':id', '**') and params. Concatenate non-empty path segments to build the view name.\n- Lazy-loaded routes: RouteConfigLoadEnd fires before RoutesRecognized; no special handling usually needed for view-name extraction since final state is captured at NavigationEnd.\n- Guard-triggered redirect (returning UrlTree): fires NavigationCancel with code=Redirect, then a new NavigationStart for the redirect target — SDK should treat only the final NavigationEnd as the committed view.\n- Config redirects ({path:'a', redirectTo:'b'}) are resolved in-flight and surfaced via urlAfterRedirects on RoutesRecognized/NavigationEnd — no cancel/restart.\n- Angular does not have a first-class 'optional segment' syntax; optional data is typically expressed via matrix params or multiple Route entries.\n- withHashLocation() is the modern way to enable hash strategy (replaces {useHash:true}).\n- SSR via @angular/ssr: the Router runs server-side; use isPlatformBrowser(PLATFORM_ID) before touching window/document in any SDK subscriber."},"terminal_reason":"completed","fast_mode_state":"off","uuid":"f0d4b1f5-6f0e-413d-ad75-9d14ae1b1463"} diff --git a/docs/integrations/angular/02-design-decisions.json b/docs/integrations/angular/02-design-decisions.json new file mode 100644 index 0000000000..4bc02af06d --- /dev/null +++ b/docs/integrations/angular/02-design-decisions.json @@ -0,0 +1 @@ +{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":175570,"duration_api_ms":171769,"num_turns":17,"result":"Design decisions complete for Angular:\n\n- **Hook**: `ResolveStart` — only hook passing all 3 priority rules (afterCancellation + afterRedirects + afterFetch:false & afterRender:false)\n- **Wrapping**: `provider` — `provideDatadogRouter()` returning `ENVIRONMENT_INITIALIZER` that uses `inject(Router).events.subscribe(...)`\n- **View name**: `matched-records` — walk `event.state.root.firstChild` concatenating `routeConfig.path`, substitute `**` with actual URL remainder\n- **Package**: `new-package` → `packages/rum-angular/`\n- **Reference**: `rum-vue` (matched-records + router-instance event subscription + dual-entry package shape)\n- **SSR**: guard with `isPlatformBrowser(inject(PLATFORM_ID))` inside the initializer","stop_reason":"end_turn","session_id":"1f4a1065-b359-4465-96a6-fda1b35e11c2","total_cost_usd":1.06183225,"usage":{"input_tokens":22,"cache_creation_input_tokens":69915,"cache_read_input_tokens":724057,"output_tokens":10509,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":69915},"inference_geo":"","iterations":[{"input_tokens":1,"output_tokens":305,"cache_read_input_tokens":56322,"cache_creation_input_tokens":4145,"cache_creation":{"ephemeral_5m_input_tokens":4145,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-opus-4-7":{"inputTokens":22,"outputTokens":10509,"cacheReadInputTokens":724057,"cacheCreationInputTokens":69915,"webSearchRequests":0,"costUSD":1.06183225,"contextWindow":200000,"maxOutputTokens":64000}},"permission_denials":[],"structured_output":{"selectedHook":{"name":"ResolveStart","rationale":"Candidates with afterCancellation:true — ResolveStart, ResolveEnd, ActivationStart, ActivationEnd, ChildActivationStart, ChildActivationEnd, NavigationEnd, NavigationSkipped, Scroll. Eliminated NavigationStart/RouteConfigLoadStart/RouteConfigLoadEnd/RoutesRecognized/GuardsCheckStart/GuardsCheckEnd/NavigationCancel/NavigationError (afterCancellation:false). Step 2 (prefer afterRedirects:true) eliminates NavigationSkipped (afterRedirects:false). Step 3 (prefer afterFetch:false AND afterRender:false) is passed only by ResolveStart (ResolveEnd, ActivationStart/End, ChildActivationStart/End, NavigationEnd all have afterFetch:true; Scroll has both afterFetch:true and afterRender:true). ResolveStart is the unique survivor: guards have already passed, redirects are applied (event.urlAfterRedirects available), RouterStateSnapshot (event.state) is populated with the final matched route tree, resolvers have not yet executed so their fetches are attributed to the new view, and component activation/render happens after so long tasks and interactions land on the new view. Note: supersession can still cancel a navigation after ResolveStart, but per the deterministic priority rules afterFetch:false wins over that edge case."},"wrappingStrategy":{"pattern":"provider","target":"A provideDatadogRouter() function returning Provider[] that registers an ENVIRONMENT_INITIALIZER (multi: true) which uses inject(Router) to subscribe to router.events, filtered by `instanceof ResolveStart`.","rationale":"Angular has no factory-wrapping convention analogous to vue-router's createRouter; the idiomatic integration surface is DI. Users already call provideRouter(routes) in bootstrapApplication; adding a sibling provideDatadogRouter() is the canonical Angular pattern (mirrors provideHttpClient, provideAnimations, etc.). ENVIRONMENT_INITIALIZER executes once per application environment at bootstrap, which is the correct lifetime for a singleton Router subscription. Using inject(Router) inside the initializer avoids circular provider ordering and works with standalone APIs (Angular 15+). A renderless component would require users to place it in their template, which is non-idiomatic for cross-cutting services. A wrap-factory over provideRouter is possible but provideRouter is not a router instance factory — the Router is instantiated lazily by DI — so wrapping the providers array adds complexity without benefit over a direct ENVIRONMENT_INITIALIZER provider."},"viewNameAlgorithm":{"family":"matched-records","rationale":"ResolveStart exposes event.state: RouterStateSnapshot. The route tree is reached via state.root (ActivatedRouteSnapshot) and traversed by walking the firstChild chain. Each ActivatedRouteSnapshot node exposes routeConfig?.path holding the literal Route pattern ('', 'users', ':id', '**', etc.). The view name is built by concatenating non-empty path segments with '/' separators — the same matched-records approach used by rum-vue (to.matched[].path) and rum-react (state.matches[].route.path). Catch-all handling: when a node has routeConfig?.path === '**', substitute the remaining actual URL segments (from event.urlAfterRedirects) for the wildcard, mirroring rum-vue's substituteCatchAll and rum-react's splat substitution. This is strictly preferred over route-id (Angular has no single parameterized-pattern string) and over param-substitution (which is lossy and heuristic)."},"targetPackage":{"mode":"new-package","package":"rum-angular"},"referenceImplementation":{"primary":"rum-vue","rationale":"Closest on all three axes: (1) subscription pattern — rum-vue subscribes to a router-instance hook (router.afterEach) analogous to Angular's router.events.subscribe; rum-react wraps a router *factory* (createBrowserRouter) which has no Angular equivalent. (2) view-name algorithm — rum-vue uses matched-records (to.matched[] path concatenation + catch-all substitution); rum-angular does the same against ActivatedRouteSnapshot.routeConfig.path. rum-react also uses matched-records but the splat syntax and matches shape differ more from Angular's than Vue's do. (3) package shape — rum-vue ships a main entry (vuePlugin) plus a router subpath entry (vue-router/createRouter); rum-angular will ship angularPlugin + a router subpath (provideDatadogRouter), the same two-entry layout. rum-nextjs is param-substitution and React-component-driven — not a match. Stage 3 should model vuePlugin.ts / startVueRouterView.ts / vueRouter.ts and adapt: swap createRouter-wrapping for an ENVIRONMENT_INITIALIZER provider, swap vue-router's RouteLocationMatched[] for walking ActivatedRouteSnapshot.firstChild, and swap Vue's '/:pathMatch(.*)*' for Angular's '**' catch-all marker."},"ssr":{"handling":"Angular supports SSR via @angular/ssr and the Router runs server-side. The ENVIRONMENT_INITIALIZER must guard its subscription so it only subscribes on the client: inside the initializer factory, call isPlatformBrowser(inject(PLATFORM_ID)) (both from @angular/common) and return early if false. This prevents server-rendered navigations from calling rumPublicApi.startView() (which would also fail because window is unavailable) and avoids duplicate view starts during client hydration — the subsequent client-side router initialization will fire its own ResolveStart for the initial route."},"notes":"\"Key implementation details for Stage 3:\\n\\n1. Event import: Angular's Router event classes live in @angular/router. Use `import { Router, ResolveStart } from '@angular/router'` and `event instanceof ResolveStart` for type narrowing.\\n\\n2. Route tree walk: At ResolveStart, event.state is a RouterStateSnapshot. Start at event.state.root (ActivatedRouteSnapshot), then walk node.firstChild recursively until null. Collect each node's routeConfig?.path, skipping empty strings and undefined. Concatenate with '/' separators, ensuring a leading '/'. This matches rum-vue/startVueRouterView.ts:computeViewName shape.\\n\\n3. Catch-all: Angular uses '**' as the wildcard path (not '*' like React Router or ':pathMatch(.*)*' like Vue). When a walked node has routeConfig?.path === '**', replace that segment with the remaining portion of event.urlAfterRedirects. Use event.urlAfterRedirects (not event.url) so redirects are already applied.\\n\\n4. Trust-but-verify: Stage 1 marked ResolveStart as afterCancellation:true because it fires after guards pass. Supersession (NavigationCancel with code=SupersededByNewNavigation) can still occur after ResolveStart — the SDK accepts this trade-off because the priority rules weight afterFetch:false higher (resource attribution correctness > occasional spurious view starts that get superseded by the next ResolveStart).\\n\\n5. Plugin surface: Mirror vuePlugin — export angularPlugin({ router?: boolean }) that sets initConfiguration.trackViewsManually = true when router:true, plus a separate entry `@datadog/browser-rum-angular/router` exporting provideDatadogRouter(). Two entry points in package.json exports map (main + router subpath) — same as rum-vue's main.ts + vueRouter.ts entries and vue-router-v4 files entry.\\n\\n6. Peer deps: `@angular/core` (for inject, PLATFORM_ID, ENVIRONMENT_INITIALIZER, Provider) and `@angular/router` (for Router, ResolveStart, RouterStateSnapshot, ActivatedRouteSnapshot) and `@angular/common` (for isPlatformBrowser) — all marked optional in peerDependenciesMeta. Support Angular 17+ (standalone-first, withComponentInputBinding era) per Stage 1 versions table.\\n\\n7. Hash routing: withHashLocation() users still trigger the same Router events, so no special handling required — urlAfterRedirects includes the hash-strategy URL in normalized form.\\n\\n8. RxJS subscription: router.events is a standard RxJS Subject. No need to import operators — use `.subscribe((event) => { if (event instanceof ResolveStart) { ... } })` to avoid pulling rxjs/operators as a dep surface. The subscription lifetime is the application environment (single-page lifetime), so no manual unsubscribe is needed — matching rum-vue's approach of leaving the afterEach registered.\\n\\n9. Unmapped Stage 1 concepts: NavigationSkipped, NavigationError, Scroll, and the lazy-load events (RouteConfigLoadStart/End) are intentionally not used — they don't add information beyond what ResolveStart provides for view tracking. Document these as 'not used' in the generated code comments if relevant, but do not add special-case handling.\""},"terminal_reason":"completed","fast_mode_state":"off","uuid":"a4aacdf6-00ad-4720-9e4e-16f6466f8453"} diff --git a/docs/integrations/angular/03-generation-manifest.md b/docs/integrations/angular/03-generation-manifest.md new file mode 100644 index 0000000000..8ce839955b --- /dev/null +++ b/docs/integrations/angular/03-generation-manifest.md @@ -0,0 +1,87 @@ +# Generation Manifest — angular + +## Generated Files + +### Phase 1: Plugin + Wrapping Strategy + +- `packages/rum-angular/package.json` — package manifest with main + `angular-router` subpath entries, optional `@angular/*` peer deps + - Reference: `packages/rum-vue/package.json` + - Deviations: peer deps are `@angular/common`, `@angular/core`, `@angular/router`, `rxjs` (all optional) instead of `vue`/`vue-router`; added `zone.js`/`tslib` to devDependencies per Angular runtime requirements. + +- `packages/rum-angular/angular-router/package.json` — subpath package entry pointer + - Reference: `packages/rum-vue/vue-router-v4/package.json` + - Deviations: none (rename-only). + +- `packages/rum-angular/angular-router/typedoc.json` — docs entry config + - Reference: `packages/rum-vue/vue-router-v4/typedoc.json` + - Deviations: none. + +- `packages/rum-angular/typedoc.json` — docs entry for main + - Reference: `packages/rum-vue/typedoc.json` + - Deviations: none. + +- `packages/rum-angular/README.md` — package overview, setup, router tracking docs + - Reference: n/a (written against design + routeSyntax.examples) + - Deviations: n/a. + +- `packages/rum-angular/src/entries/main.ts` — public main entry exporting `angularPlugin` and config types + - Reference: `packages/rum-vue/src/entries/main.ts` + - Deviations: none (rename-only). + +- `packages/rum-angular/src/entries/angularRouter.ts` — router subpath entry exporting `provideDatadogRouter` + - Reference: `packages/rum-vue/src/entries/vueRouter.ts` + - Deviations: exports `provideDatadogRouter` (DI provider array) instead of `createRouter` (factory wrapper), per `wrappingStrategy.pattern = "provider"`. + +- `packages/rum-angular/src/domain/angularPlugin.ts` — plugin lifecycle, subscriber pattern, configuration + - Reference: `packages/rum-vue/src/domain/vuePlugin.ts` + - Deviations: identifier renames only (`vue`→`angular`). + +- `packages/rum-angular/src/domain/angularPlugin.spec.ts` — plugin structure tests + - Reference: `packages/rum-vue/src/domain/vuePlugin.spec.ts` + - Deviations: identifier renames only. + +- `packages/rum-angular/src/domain/angularRouter/provideDatadogRouter.ts` — integration point: `ENVIRONMENT_INITIALIZER` provider subscribing to `Router.events` and filtering by `ResolveStart` + - Reference: `packages/rum-vue/src/domain/router/vueRouter.ts` + - Deviations: per `wrappingStrategy.pattern = "provider"` — DI provider array instead of factory wrap. Uses `inject(Router)` and `isPlatformBrowser(inject(PLATFORM_ID))` for SSR guard (design `ssr.handling`). Filter uses a `isResolveStart` type predicate over `EventType.ResolveStart` (instead of `instanceof ResolveStart`) so `startAngularRouterTracking` is testable with plain-object fakes, preserving design intent (same runtime semantics). + +- `packages/rum-angular/src/domain/angularRouter/provideDatadogRouter.spec.ts` — provider + subscription tests + - Reference: `packages/rum-vue/src/domain/router/vueRouter.spec.ts` + - Deviations: test fake is a minimal `events.subscribe` object (vue spec uses a real `createRouter` + `createMemoryHistory`). Reason: Angular's `Router` requires a full DI bootstrap; the provider shape is covered with a dedicated `provideDatadogRouter` suite. First line imports `@angular/compiler` to satisfy JIT requirement when `@angular/router` modules load under Karma. + +- `packages/rum-angular/test/initializeAngularPlugin.ts` — shared test helper + - Reference: `packages/rum-vue/test/initializeVuePlugin.ts` + - Deviations: identifier renames only. + +### Phase 2: View Name Algorithm + +- `packages/rum-angular/src/domain/angularRouter/startAngularView.ts` — `computeViewName()` + `startAngularRouterView()` walking `ActivatedRouteSnapshot.firstChild` chain and substituting `**` catch-all + - Reference: `packages/rum-vue/src/domain/router/startVueRouterView.ts` + - Deviations: input is a root `ActivatedRouteSnapshot` with `firstChild` chain (walked recursively) instead of Vue's flat `matched[]` array. Catch-all marker is `/**` (Angular) instead of `/:pathMatch(.*)*` (Vue Router). `substituteCatchAll` additionally strips query/fragment from the URL before splitting, matching Angular's `urlAfterRedirects` format. + +- `packages/rum-angular/src/domain/angularRouter/types.ts` — minimal `AngularActivatedRouteSnapshot` local type + - Reference: n/a (no Vue analogue — Vue imports `RouteLocationMatched` at runtime since `vue-router` is allowlisted for side-effects) + - Deviations: local type mirrors only `routeConfig.path` and `firstChild` to avoid runtime coupling to `@angular/router` in the main entry and keep tests simple. + +- `packages/rum-angular/src/domain/angularRouter/startAngularView.spec.ts` — view name tests + - Reference: `packages/rum-vue/src/domain/router/startVueRouterView.spec.ts` + - Deviations: test helper builds a snapshot tree via nested `firstChild` links (instead of a `matched[]` array). Test cases translated from `routeSyntax.examples`: static paths, dynamic `:id`, nested routes, `**` catch-all (with + without prefix params), and URL query/fragment stripping. Every `routeSyntax.concept` has coverage. + +## Validation + +**Status:** pass +**Iterations:** 2 / 5 + +### Iteration 1 — typecheck fail, lint fail, unit tests not reached + +- Typecheck: `RouterEventsEmitter.events.subscribe` typed its observer parameter as `Event` (from `@angular/router`), incompatible with the test fake's `(event: unknown) => void` subscriber under `strictFunctionTypes`. Fixed by widening the callback parameter to `unknown` and replacing the inline type assertion with an `isResolveStart` type-predicate function. +- Lint `local-rules/disallow-side-effects`: `@angular/core`, `@angular/common`, `@angular/router` are not in the repo's allowlist of side-effect-free packages. Added all three alongside the existing `vue`/`vue-router`/`react` entries in `eslint-local-rules/disallowSideEffects.js` — the canonical precedent for adding a new framework integration. +- Lint `@typescript-eslint/no-unnecessary-type-assertion`: `event as ResolveStart` became redundant once the type predicate was in place — removed. +- Lint `@typescript-eslint/no-empty-function`: fake router's `unsubscribe() {}` method replaced with `unsubscribe: noop` (imported from `@datadog/browser-core`). +- Lint `prefer-const`: changed `let root` to `const root` in the fake-event builder (only the reference is reassigned; the object literal itself is not). + +### Iteration 2 — typecheck fail, lint pass, unit tests fail + +- Unit tests: Karma load failed with `JIT compilation failed for injectable PlatformNavigation` because importing `@angular/router` in the spec file transitively pulls `@angular/common`'s partially-compiled decorators that need the JIT compiler. Added `import '@angular/compiler'` as the first import of the spec file. `@angular/compiler` was already a devDependency of `rum-angular`. + +### Iteration 3 — typecheck pass, lint pass, unit tests pass (37 / 37) + From c5882a10700c0916f8c0bcf14f85a16d36cfb39a Mon Sep 17 00:00:00 2001 From: Aymeric Mortemousque Date: Fri, 17 Apr 2026 14:54:25 +0200 Subject: [PATCH 12/12] =?UTF-8?q?=E2=9C=A8=20Add=20angular=20router=20inte?= =?UTF-8?q?gration=20package?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auto-generated from framework documentation using the router integration pipeline. See docs/integrations/angular/ for design artifacts and decision rationale. --- eslint-local-rules/disallowSideEffects.js | 3 + packages/rum-angular/LICENSE | 201 ++++++++++++++++++ packages/rum-angular/README.md | 106 +++++++++ .../rum-angular/angular-router/package.json | 7 + .../rum-angular/angular-router/typedoc.json | 4 + packages/rum-angular/package.json | 71 +++++++ .../src/domain/angularPlugin.spec.ts | 48 +++++ .../rum-angular/src/domain/angularPlugin.ts | 68 ++++++ .../provideDatadogRouter.spec.ts | 132 ++++++++++++ .../angularRouter/provideDatadogRouter.ts | 69 ++++++ .../angularRouter/startAngularView.spec.ts | 101 +++++++++ .../domain/angularRouter/startAngularView.ts | 65 ++++++ .../src/domain/angularRouter/types.ts | 8 + .../rum-angular/src/entries/angularRouter.ts | 1 + packages/rum-angular/src/entries/main.ts | 2 + .../test/initializeAngularPlugin.ts | 26 +++ packages/rum-angular/typedoc.json | 4 + tsconfig.base.json | 3 + yarn.lock | 121 ++++++++++- 19 files changed, 1039 insertions(+), 1 deletion(-) create mode 100644 packages/rum-angular/LICENSE create mode 100644 packages/rum-angular/README.md create mode 100644 packages/rum-angular/angular-router/package.json create mode 100644 packages/rum-angular/angular-router/typedoc.json create mode 100644 packages/rum-angular/package.json create mode 100644 packages/rum-angular/src/domain/angularPlugin.spec.ts create mode 100644 packages/rum-angular/src/domain/angularPlugin.ts create mode 100644 packages/rum-angular/src/domain/angularRouter/provideDatadogRouter.spec.ts create mode 100644 packages/rum-angular/src/domain/angularRouter/provideDatadogRouter.ts create mode 100644 packages/rum-angular/src/domain/angularRouter/startAngularView.spec.ts create mode 100644 packages/rum-angular/src/domain/angularRouter/startAngularView.ts create mode 100644 packages/rum-angular/src/domain/angularRouter/types.ts create mode 100644 packages/rum-angular/src/entries/angularRouter.ts create mode 100644 packages/rum-angular/src/entries/main.ts create mode 100644 packages/rum-angular/test/initializeAngularPlugin.ts create mode 100644 packages/rum-angular/typedoc.json diff --git a/eslint-local-rules/disallowSideEffects.js b/eslint-local-rules/disallowSideEffects.js index 2ade75eb10..36debfb995 100644 --- a/eslint-local-rules/disallowSideEffects.js +++ b/eslint-local-rules/disallowSideEffects.js @@ -40,6 +40,9 @@ const packagesWithoutSideEffect = new Set([ 'react-router-dom', 'vue', 'vue-router', + '@angular/common', + '@angular/core', + '@angular/router', ]) /** diff --git a/packages/rum-angular/LICENSE b/packages/rum-angular/LICENSE new file mode 100644 index 0000000000..e6d7fbc979 --- /dev/null +++ b/packages/rum-angular/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2019-Present Datadog, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/rum-angular/README.md b/packages/rum-angular/README.md new file mode 100644 index 0000000000..a33290dda1 --- /dev/null +++ b/packages/rum-angular/README.md @@ -0,0 +1,106 @@ +# RUM Browser Monitoring - Angular integration + +**Note**: This integration is in Preview. Features and configuration are subject to change. + +## Overview + +The Datadog RUM Angular integration provides framework-specific instrumentation to help you monitor and debug Angular applications. This integration adds: + +- **Automatic route change detection** using Angular Router +- **View name normalization** that maps dynamic route segments to their parameterized definitions (e.g. `/users/123` becomes `/users/:id`) +- **Full-stack visibility** by correlating frontend performance with backend traces and logs + +Combined with Datadog RUM's core capabilities, you can debug performance bottlenecks, track user journeys, monitor Core Web Vitals, and analyze every user session with context. + +## Setup + +Start by setting up [Datadog RUM][1] in your Angular application. + +This integration requires **Angular 17+**. + +## Basic usage + +### 1. Initialize the Datadog RUM SDK with the Angular plugin + +In your `main.ts`: + +```ts +import { datadogRum } from '@datadog/browser-rum' +import { angularPlugin } from '@datadog/browser-rum-angular' + +datadogRum.init({ + applicationId: '', + clientToken: '', + site: 'datadoghq.com', + plugins: [angularPlugin()], +}) +``` + +## Router view tracking + +To automatically track route changes as RUM views, enable the `router` option in the plugin and register the Datadog router providers alongside `provideRouter()`. + +### 1. Initialize the RUM SDK with router tracking enabled + +```ts +import { datadogRum } from '@datadog/browser-rum' +import { angularPlugin } from '@datadog/browser-rum-angular' + +datadogRum.init({ + applicationId: '', + clientToken: '', + site: 'datadoghq.com', + plugins: [angularPlugin({ router: true })], +}) +``` + +### 2. Register the Datadog router providers + +Add `provideDatadogRouter()` to your `bootstrapApplication` providers, next to `provideRouter()`: + +```ts +import { bootstrapApplication } from '@angular/platform-browser' +import { provideRouter } from '@angular/router' +import { provideDatadogRouter } from '@datadog/browser-rum-angular/angular-router' +import { AppComponent } from './app/app.component' +import { routes } from './app/routes' + +bootstrapApplication(AppComponent, { + providers: [provideRouter(routes), provideDatadogRouter()], +}) +``` + +## Route tracking + +Router tracking listens for the `ResolveStart` router event (after guards and redirects, before resolvers execute), and normalizes dynamic segments into parameterized view names: + +| Actual URL | View name | +| ---------------------- | -------------------------- | +| `/about` | `/about` | +| `/users/123` | `/users/:id` | +| `/users/123/posts/456` | `/users/:id/posts/:postId` | +| `/docs/a/b/c` | `/docs/a/b/c` | + +## Go further with Datadog Angular integration + +### Traces + +Connect your RUM and trace data to get a complete view of your application's performance. See [Connect RUM and Traces][3]. + +### Logs + +To forward your Angular application's logs to Datadog, see [JavaScript Logs Collection][4]. + +### Metrics + +To generate custom metrics from your RUM application, see [Generate Metrics][5]. + +## Troubleshooting + +Need help? Contact [Datadog Support][6]. + +[1]: https://docs.datadoghq.com/real_user_monitoring/browser/setup/client +[3]: https://docs.datadoghq.com/real_user_monitoring/platform/connect_rum_and_traces/?tab=browserrum +[4]: https://docs.datadoghq.com/logs/log_collection/javascript/ +[5]: https://docs.datadoghq.com/real_user_monitoring/generate_metrics +[6]: https://docs.datadoghq.com/help/ diff --git a/packages/rum-angular/angular-router/package.json b/packages/rum-angular/angular-router/package.json new file mode 100644 index 0000000000..15c9eb1fa7 --- /dev/null +++ b/packages/rum-angular/angular-router/package.json @@ -0,0 +1,7 @@ +{ + "name": "@datadog/browser-rum-angular/angular-router", + "private": true, + "main": "../cjs/entries/angularRouter.js", + "module": "../esm/entries/angularRouter.js", + "types": "../cjs/entries/angularRouter.d.ts" +} diff --git a/packages/rum-angular/angular-router/typedoc.json b/packages/rum-angular/angular-router/typedoc.json new file mode 100644 index 0000000000..4781794a37 --- /dev/null +++ b/packages/rum-angular/angular-router/typedoc.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["../src/entries/angularRouter.ts"] +} diff --git a/packages/rum-angular/package.json b/packages/rum-angular/package.json new file mode 100644 index 0000000000..09375de42e --- /dev/null +++ b/packages/rum-angular/package.json @@ -0,0 +1,71 @@ +{ + "name": "@datadog/browser-rum-angular", + "version": "6.32.0", + "license": "Apache-2.0", + "main": "cjs/entries/main.js", + "module": "esm/entries/main.js", + "types": "cjs/entries/main.d.ts", + "files": [ + "cjs", + "esm", + "src", + "angular-router", + "!src/**/*.spec.ts", + "!src/**/*.specHelper.ts" + ], + "scripts": { + "build": "node ../../scripts/build/build-package.ts --modules", + "prepack": "yarn build" + }, + "dependencies": { + "@datadog/browser-core": "6.32.0", + "@datadog/browser-rum-core": "6.32.0" + }, + "peerDependencies": { + "@angular/common": ">=17.0.0", + "@angular/core": ">=17.0.0", + "@angular/router": ">=17.0.0", + "rxjs": ">=7.0.0" + }, + "peerDependenciesMeta": { + "@angular/common": { + "optional": true + }, + "@angular/core": { + "optional": true + }, + "@angular/router": { + "optional": true + }, + "@datadog/browser-rum": { + "optional": true + }, + "@datadog/browser-rum-slim": { + "optional": true + }, + "rxjs": { + "optional": true + } + }, + "devDependencies": { + "@angular/common": "17.3.12", + "@angular/compiler": "17.3.12", + "@angular/core": "17.3.12", + "@angular/platform-browser": "17.3.12", + "@angular/router": "17.3.12", + "rxjs": "7.8.1", + "tslib": "2.8.1", + "zone.js": "0.14.10" + }, + "repository": { + "type": "git", + "url": "https://github.com/DataDog/browser-sdk.git", + "directory": "packages/rum-angular" + }, + "volta": { + "extends": "../../package.json" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/rum-angular/src/domain/angularPlugin.spec.ts b/packages/rum-angular/src/domain/angularPlugin.spec.ts new file mode 100644 index 0000000000..3becb5c3d5 --- /dev/null +++ b/packages/rum-angular/src/domain/angularPlugin.spec.ts @@ -0,0 +1,48 @@ +import type { RumInitConfiguration, RumPublicApi } from '@datadog/browser-rum-core' +import { registerCleanupTask } from '../../../core/test' +import { onRumInit, angularPlugin, resetAngularPlugin } from './angularPlugin' + +const PUBLIC_API = {} as RumPublicApi +const INIT_CONFIGURATION = {} as RumInitConfiguration + +describe('angularPlugin', () => { + beforeEach(() => { + registerCleanupTask(() => resetAngularPlugin()) + }) + + it('returns a plugin object with name "angular"', () => { + expect(angularPlugin()).toEqual(jasmine.objectContaining({ name: 'angular' })) + }) + + it('calls callbacks registered with onRumInit during onInit', () => { + const spy = jasmine.createSpy() + const config = {} + onRumInit(spy) + angularPlugin(config).onInit({ publicApi: PUBLIC_API, initConfiguration: INIT_CONFIGURATION }) + expect(spy).toHaveBeenCalledOnceWith(config, PUBLIC_API) + }) + + it('calls callbacks immediately if onInit was already invoked', () => { + const spy = jasmine.createSpy() + const config = {} + angularPlugin(config).onInit({ publicApi: PUBLIC_API, initConfiguration: INIT_CONFIGURATION }) + onRumInit(spy) + expect(spy).toHaveBeenCalledOnceWith(config, PUBLIC_API) + }) + + it('sets trackViewsManually when router is true', () => { + const initConfiguration = { ...INIT_CONFIGURATION } + angularPlugin({ router: true }).onInit({ publicApi: PUBLIC_API, initConfiguration }) + expect(initConfiguration.trackViewsManually).toBe(true) + }) + + it('does not set trackViewsManually when router is false', () => { + const initConfiguration = { ...INIT_CONFIGURATION } + angularPlugin({ router: false }).onInit({ publicApi: PUBLIC_API, initConfiguration }) + expect(initConfiguration.trackViewsManually).toBeUndefined() + }) + + it('returns configuration telemetry', () => { + expect(angularPlugin({ router: true }).getConfigurationTelemetry()).toEqual({ router: true }) + }) +}) diff --git a/packages/rum-angular/src/domain/angularPlugin.ts b/packages/rum-angular/src/domain/angularPlugin.ts new file mode 100644 index 0000000000..b5677dbbb7 --- /dev/null +++ b/packages/rum-angular/src/domain/angularPlugin.ts @@ -0,0 +1,68 @@ +import type { RumPlugin, RumPublicApi, StartRumResult } from '@datadog/browser-rum-core' + +let globalPublicApi: RumPublicApi | undefined +let globalConfiguration: AngularPluginConfiguration | undefined +let globalAddError: StartRumResult['addError'] | undefined + +type InitSubscriber = (configuration: AngularPluginConfiguration, rumPublicApi: RumPublicApi) => void +type StartSubscriber = (addError: StartRumResult['addError']) => void + +const onRumInitSubscribers: InitSubscriber[] = [] +const onRumStartSubscribers: StartSubscriber[] = [] + +export interface AngularPluginConfiguration { + router?: boolean +} + +export type AngularPlugin = Required + +export function angularPlugin(configuration: AngularPluginConfiguration = {}): AngularPlugin { + return { + name: 'angular', + onInit({ publicApi, initConfiguration }) { + globalPublicApi = publicApi + globalConfiguration = configuration + for (const subscriber of onRumInitSubscribers) { + subscriber(globalConfiguration, globalPublicApi) + } + if (configuration.router) { + initConfiguration.trackViewsManually = true + } + }, + onRumStart({ addError }) { + globalAddError = addError + if (addError) { + for (const subscriber of onRumStartSubscribers) { + subscriber(addError) + } + } + }, + getConfigurationTelemetry() { + return { router: !!configuration.router } + }, + } satisfies RumPlugin +} + +export function onRumInit(callback: InitSubscriber) { + if (globalConfiguration && globalPublicApi) { + callback(globalConfiguration, globalPublicApi) + } else { + onRumInitSubscribers.push(callback) + } +} + +export function onRumStart(callback: StartSubscriber) { + if (globalAddError) { + callback(globalAddError) + } else { + onRumStartSubscribers.push(callback) + } +} + +export function resetAngularPlugin() { + globalPublicApi = undefined + globalConfiguration = undefined + globalAddError = undefined + onRumInitSubscribers.length = 0 + onRumStartSubscribers.length = 0 +} diff --git a/packages/rum-angular/src/domain/angularRouter/provideDatadogRouter.spec.ts b/packages/rum-angular/src/domain/angularRouter/provideDatadogRouter.spec.ts new file mode 100644 index 0000000000..86603a61b0 --- /dev/null +++ b/packages/rum-angular/src/domain/angularRouter/provideDatadogRouter.spec.ts @@ -0,0 +1,132 @@ +// Importing `@angular/router` at runtime pulls transitive providers (e.g. PlatformNavigation) +// that rely on Angular's JIT compiler when not AOT-linked. Load the compiler first so the +// partial-compiled decorators can be instantiated inside Karma. +import '@angular/compiler' +import { ENVIRONMENT_INITIALIZER } from '@angular/core' +import { EventType } from '@angular/router' +import { noop } from '@datadog/browser-core' +import { initializeAngularPlugin } from '../../../test/initializeAngularPlugin' +import { provideDatadogRouter, startAngularRouterTracking } from './provideDatadogRouter' + +type EventListener = (event: unknown) => void + +function createFakeRouter() { + const listeners: EventListener[] = [] + return { + events: { + subscribe(next: EventListener) { + listeners.push(next) + return { unsubscribe: noop } + }, + }, + emit(event: unknown) { + for (const listener of listeners) { + listener(event) + } + }, + } +} + +function buildResolveStart(paths: string[], urlAfterRedirects: string) { + const root: { routeConfig: { path: string } | null; firstChild: unknown } = { + routeConfig: null, + firstChild: null, + } + let current = root + for (const path of paths) { + const next = { routeConfig: { path }, firstChild: null as unknown } + current.firstChild = next + current = next as typeof root + } + return { + type: EventType.ResolveStart, + state: { root }, + urlAfterRedirects, + } +} + +describe('provideDatadogRouter', () => { + it('returns a single ENVIRONMENT_INITIALIZER multi-provider', () => { + const providers = provideDatadogRouter() as Array<{ provide: unknown; multi?: boolean; useValue?: unknown }> + expect(providers.length).toBe(1) + expect(providers[0].provide).toBe(ENVIRONMENT_INITIALIZER) + expect(providers[0].multi).toBe(true) + expect(typeof providers[0].useValue).toBe('function') + }) +}) + +describe('startAngularRouterTracking', () => { + it('calls startView on ResolveStart with the computed view name', () => { + const startViewSpy = jasmine.createSpy() + initializeAngularPlugin({ + configuration: { router: true }, + publicApi: { startView: startViewSpy }, + }) + + const router = createFakeRouter() + startAngularRouterTracking(router) + router.emit(buildResolveStart(['users', ':id'], '/users/42')) + + expect(startViewSpy).toHaveBeenCalledOnceWith('/users/:id') + }) + + it('ignores non-ResolveStart events', () => { + const startViewSpy = jasmine.createSpy() + initializeAngularPlugin({ + configuration: { router: true }, + publicApi: { startView: startViewSpy }, + }) + + const router = createFakeRouter() + startAngularRouterTracking(router) + router.emit({ type: EventType.NavigationStart, id: 1, url: '/users/42' }) + router.emit({ type: EventType.NavigationEnd, id: 1, url: '/users/42', urlAfterRedirects: '/users/42' }) + + expect(startViewSpy).not.toHaveBeenCalled() + }) + + it('tracks each navigation independently', () => { + const startViewSpy = jasmine.createSpy() + initializeAngularPlugin({ + configuration: { router: true }, + publicApi: { startView: startViewSpy }, + }) + + const router = createFakeRouter() + startAngularRouterTracking(router) + router.emit(buildResolveStart([''], '/')) + router.emit(buildResolveStart(['about'], '/about')) + + expect(startViewSpy).toHaveBeenCalledTimes(2) + expect(startViewSpy.calls.allArgs()).toEqual([['/'], ['/about']]) + }) + + it('substitutes catch-all pattern with the actual path', () => { + const startViewSpy = jasmine.createSpy() + initializeAngularPlugin({ + configuration: { router: true }, + publicApi: { startView: startViewSpy }, + }) + + const router = createFakeRouter() + startAngularRouterTracking(router) + router.emit(buildResolveStart(['**'], '/unknown/page')) + + expect(startViewSpy).toHaveBeenCalledOnceWith('/unknown/page') + }) + + it('uses urlAfterRedirects for view name after a redirect', () => { + const startViewSpy = jasmine.createSpy() + initializeAngularPlugin({ + configuration: { router: true }, + publicApi: { startView: startViewSpy }, + }) + + const router = createFakeRouter() + startAngularRouterTracking(router) + // Initial URL '/old', redirected to '/new' — the matched tree belongs to the redirect target. + router.emit(buildResolveStart(['new'], '/new')) + + expect(startViewSpy).toHaveBeenCalledOnceWith('/new') + }) +}) diff --git a/packages/rum-angular/src/domain/angularRouter/provideDatadogRouter.ts b/packages/rum-angular/src/domain/angularRouter/provideDatadogRouter.ts new file mode 100644 index 0000000000..a594ced21a --- /dev/null +++ b/packages/rum-angular/src/domain/angularRouter/provideDatadogRouter.ts @@ -0,0 +1,69 @@ +import { ENVIRONMENT_INITIALIZER, PLATFORM_ID, inject } from '@angular/core' +import type { Provider } from '@angular/core' +import { isPlatformBrowser } from '@angular/common' +import { EventType, Router } from '@angular/router' +import type { ResolveStart } from '@angular/router' +import { startAngularRouterView } from './startAngularView' +import type { AngularActivatedRouteSnapshot } from './types' + +/** + * Minimal shape of the Angular `Router` service that the tracking function consumes. + * Using this internal type lets us test `startAngularRouterTracking` without spinning + * up an Angular DI container. + */ +interface RouterEventsEmitter { + events: { + subscribe: (observer: (event: unknown) => void) => unknown + } +} + +/** + * Returns the providers needed to enable Datadog RUM view tracking for Angular Router. + * + * Add this alongside your `provideRouter` call in `bootstrapApplication` (or in a module's + * providers list). It installs an `ENVIRONMENT_INITIALIZER` that subscribes to the Router's + * `ResolveStart` events and calls `startView` on the RUM public API for each committed + * navigation. + * + * The subscription is skipped during server-side rendering (no browser platform). + */ +export function provideDatadogRouter(): Provider[] { + return [ + { + provide: ENVIRONMENT_INITIALIZER, + multi: true, + useValue: initializeDatadogRouterTracking, + }, + ] +} + +function initializeDatadogRouterTracking() { + // On the server (@angular/ssr), window is unavailable and a fresh ResolveStart + // will fire again during client hydration — avoid subscribing twice. + if (!isPlatformBrowser(inject(PLATFORM_ID))) { + return + } + startAngularRouterTracking(inject(Router)) +} + +/** + * Subscribes to a Router's events and triggers a RUM view start for each committed + * `ResolveStart`. Exported for testing — production code should use `provideDatadogRouter()`. + * + * ResolveStart is chosen over NavigationEnd so that resolver fetches and initial + * component render work are attributed to the new view rather than the previous one. + * Supersession can still cancel a navigation after this point; the next navigation's + * ResolveStart supersedes it. + */ +export function startAngularRouterTracking(router: RouterEventsEmitter) { + router.events.subscribe((event) => { + if (isResolveStart(event)) { + const root = event.state.root as unknown as AngularActivatedRouteSnapshot + startAngularRouterView(root, event.urlAfterRedirects) + } + }) +} + +function isResolveStart(event: unknown): event is ResolveStart { + return typeof event === 'object' && event !== null && (event as { type?: unknown }).type === EventType.ResolveStart +} diff --git a/packages/rum-angular/src/domain/angularRouter/startAngularView.spec.ts b/packages/rum-angular/src/domain/angularRouter/startAngularView.spec.ts new file mode 100644 index 0000000000..7538d2b0dc --- /dev/null +++ b/packages/rum-angular/src/domain/angularRouter/startAngularView.spec.ts @@ -0,0 +1,101 @@ +import { display } from '@datadog/browser-core' +import { initializeAngularPlugin } from '../../../test/initializeAngularPlugin' +import { startAngularRouterView, computeViewName } from './startAngularView' +import type { AngularActivatedRouteSnapshot } from './types' + +/** + * Build an ActivatedRouteSnapshot-like tree from a flat list of route paths. The first entry + * corresponds to the root's firstChild (Angular's root snapshot has no routeConfig), subsequent + * entries nest as firstChild chains. Passing `undefined` produces a node with no routeConfig. + */ +function buildSnapshot(paths: Array): AngularActivatedRouteSnapshot { + const nodes: AngularActivatedRouteSnapshot[] = paths.map((path) => ({ + routeConfig: path === undefined ? null : { path }, + firstChild: null, + })) + for (let i = 0; i < nodes.length - 1; i++) { + nodes[i].firstChild = nodes[i + 1] + } + const root: AngularActivatedRouteSnapshot = { + routeConfig: null, + firstChild: nodes[0] ?? null, + } + return root +} + +describe('startAngularRouterView', () => { + it('starts a new view with the computed view name', () => { + const startViewSpy = jasmine.createSpy() + initializeAngularPlugin({ + configuration: { router: true }, + publicApi: { startView: startViewSpy }, + }) + + startAngularRouterView(buildSnapshot(['user', ':id']), '/user/1') + + expect(startViewSpy).toHaveBeenCalledOnceWith('/user/:id') + }) + + it('warns if router: true is missing from plugin config', () => { + const warnSpy = spyOn(display, 'warn') + initializeAngularPlugin({ configuration: {} }) + startAngularRouterView(buildSnapshot([]), '/') + expect(warnSpy).toHaveBeenCalledOnceWith( + '`router: true` is missing from the angular plugin configuration, the view will not be tracked.' + ) + }) +}) + +describe('computeViewName', () => { + it('returns an empty string if the root is null', () => { + expect(computeViewName(null, '/')).toBe('') + }) + + it('returns "/" if the tree has no routeConfig.path segments', () => { + expect(computeViewName(buildSnapshot([undefined, '']), '/')).toBe('/') + }) + + it('ignores routes with an empty path (Angular componentless/group routes)', () => { + expect(computeViewName(buildSnapshot(['foo', '', ':id']), '/foo/1')).toBe('/foo/:id') + }) + + // prettier-ignore + // Angular's ActivatedRouteSnapshot exposes routeConfig.path which holds the literal Route + // pattern ('', 'users', ':id', '**', etc.). Paths are concatenated with '/' separators. + // Catch-all uses '**' (not bare '*' like React Router or '/:pathMatch(.*)*' like Vue). + const cases: Array<[string, Array, string, string]> = [ + // description, segments, path, expected + + // Simple paths + ['single static segment', ['foo'], '/foo', '/foo'], + ['nested static segments', ['foo', 'bar'], '/foo/bar', '/foo/bar'], + ['nested with param', ['foo', 'bar', ':p'], '/foo/bar/1', '/foo/bar/:p'], + ['root param', [':p'], '/foo', '/:p'], + ['param in single segment', ['foo/:p'], '/foo/bar', '/foo/:p'], + ['nested param', ['foo', ':p'], '/foo/bar', '/foo/:p'], + ['multiple params', [':a/:b'], '/foo/bar', '/:a/:b'], + ['nested multiple params', [':a', ':b'], '/foo/bar', '/:a/:b'], + ['param with prefix', ['foo-:a'], '/foo-1', '/foo-:a'], + ['empty root with child', ['', 'users'], '/users', '/users'], + ['empty root with param child', ['', ':id'], '/42', '/:id'], + + // Catch-all routes (Angular uses '**') + ['catch-all at root', ['**'], '/foo/1', '/foo/1'], + ['catch-all at root (index)', ['**'], '/', '/'], + ['nested catch-all', ['foo', '**'], '/foo/1', '/foo/1'], + ['deeply nested catch-all', ['foo', 'bar', '**'], '/foo/bar/baz', '/foo/bar/baz'], + ['static sibling before catch-all', ['foo', '**'], '/foo/bar', '/foo/bar'], + ['param before catch-all', [':p', '**'], '/foo/baz', '/:p/baz'], + ['multiple params before catch-all', ['org', ':orgId', '**'], '/org/123/some/page', '/org/:orgId/some/page'], + + // URL variants + ['strips query string in catch-all', ['**'], '/foo/bar?x=1', '/foo/bar'], + ['strips fragment in catch-all', ['**'], '/foo/bar#anchor', '/foo/bar'], + ] + + cases.forEach(([description, segments, path, expected]) => { + it(`returns "${expected}" for ${description}`, () => { + expect(computeViewName(buildSnapshot(segments), path)).toBe(expected) + }) + }) +}) diff --git a/packages/rum-angular/src/domain/angularRouter/startAngularView.ts b/packages/rum-angular/src/domain/angularRouter/startAngularView.ts new file mode 100644 index 0000000000..ccb99380ef --- /dev/null +++ b/packages/rum-angular/src/domain/angularRouter/startAngularView.ts @@ -0,0 +1,65 @@ +import { display } from '@datadog/browser-core' +import { onRumInit } from '../angularPlugin' +import type { AngularActivatedRouteSnapshot } from './types' + +export function startAngularRouterView(root: AngularActivatedRouteSnapshot, urlAfterRedirects: string) { + onRumInit((configuration, rumPublicApi) => { + if (!configuration.router) { + display.warn('`router: true` is missing from the angular plugin configuration, the view will not be tracked.') + return + } + rumPublicApi.startView(computeViewName(root, urlAfterRedirects)) + }) +} + +export function computeViewName(root: AngularActivatedRouteSnapshot | null, urlAfterRedirects: string): string { + if (!root) { + return '' + } + + let viewName = '/' + let node: AngularActivatedRouteSnapshot | null = root.firstChild + + while (node) { + const routePath = node.routeConfig?.path + if (routePath) { + if (!viewName.endsWith('/')) { + viewName += '/' + } + viewName += routePath + } + node = node.firstChild + } + + return substituteCatchAll(viewName, urlAfterRedirects) +} + +/** + * Angular uses `**` as the catch-all wildcard path. Keeping the raw pattern as the view + * name isn't useful, it hides which path was actually visited. This function replaces + * only the catch-all segment with the corresponding portion of the actual URL, preserving + * any parameterized segments that precede it. This aligns with how the Vue and React + * integrations substitute their catch-all patterns. + * + * @example + * substituteCatchAll('/**', '/unknown/page') // => '/unknown/page' + * substituteCatchAll('/org/:orgId/**', '/org/123/some/page') // => '/org/:orgId/some/page' + */ +function substituteCatchAll(viewName: string, path: string): string { + const catchAllIndex = viewName.indexOf('/**') + if (catchAllIndex === -1) { + return viewName + } + + const prefix = viewName.substring(0, catchAllIndex) + const prefixSegmentCount = prefix === '' ? 0 : prefix.split('/').length - 1 + const pathSegments = stripQueryAndFragment(path).split('/') + const suffix = pathSegments.slice(prefixSegmentCount + 1).join('/') + + return prefix + (suffix ? `/${suffix}` : '') || '/' +} + +function stripQueryAndFragment(path: string): string { + const queryIndex = path.search(/[?#]/) + return queryIndex === -1 ? path : path.substring(0, queryIndex) +} diff --git a/packages/rum-angular/src/domain/angularRouter/types.ts b/packages/rum-angular/src/domain/angularRouter/types.ts new file mode 100644 index 0000000000..3ffdee8e4c --- /dev/null +++ b/packages/rum-angular/src/domain/angularRouter/types.ts @@ -0,0 +1,8 @@ +// Minimal local types mirroring the subset of @angular/router we actually consume. +// Defined here to avoid a runtime dependency on @angular/router for consumers that +// only use the main plugin entry. + +export interface AngularActivatedRouteSnapshot { + routeConfig: { path?: string } | null + firstChild: AngularActivatedRouteSnapshot | null +} diff --git a/packages/rum-angular/src/entries/angularRouter.ts b/packages/rum-angular/src/entries/angularRouter.ts new file mode 100644 index 0000000000..98284f3a9d --- /dev/null +++ b/packages/rum-angular/src/entries/angularRouter.ts @@ -0,0 +1 @@ +export { provideDatadogRouter } from '../domain/angularRouter/provideDatadogRouter' diff --git a/packages/rum-angular/src/entries/main.ts b/packages/rum-angular/src/entries/main.ts new file mode 100644 index 0000000000..d1f80401ca --- /dev/null +++ b/packages/rum-angular/src/entries/main.ts @@ -0,0 +1,2 @@ +export type { AngularPluginConfiguration, AngularPlugin } from '../domain/angularPlugin' +export { angularPlugin } from '../domain/angularPlugin' diff --git a/packages/rum-angular/test/initializeAngularPlugin.ts b/packages/rum-angular/test/initializeAngularPlugin.ts new file mode 100644 index 0000000000..0edefd47fc --- /dev/null +++ b/packages/rum-angular/test/initializeAngularPlugin.ts @@ -0,0 +1,26 @@ +import type { RumInitConfiguration, RumPublicApi, StartRumResult } from '@datadog/browser-rum-core' +import { noop } from '@datadog/browser-core' +import type { AngularPluginConfiguration } from '../src/domain/angularPlugin' +import { angularPlugin, resetAngularPlugin } from '../src/domain/angularPlugin' +import { registerCleanupTask } from '../../core/test' + +export function initializeAngularPlugin({ + configuration = {}, + initConfiguration = {}, + publicApi = {}, + addError = noop, +}: { + configuration?: AngularPluginConfiguration + initConfiguration?: Partial + publicApi?: Partial + addError?: StartRumResult['addError'] +} = {}) { + resetAngularPlugin() + const plugin = angularPlugin(configuration) + plugin.onInit({ + publicApi: publicApi as RumPublicApi, + initConfiguration: initConfiguration as RumInitConfiguration, + }) + plugin.onRumStart({ addError }) + registerCleanupTask(() => resetAngularPlugin()) +} diff --git a/packages/rum-angular/typedoc.json b/packages/rum-angular/typedoc.json new file mode 100644 index 0000000000..002b26a53c --- /dev/null +++ b/packages/rum-angular/typedoc.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["src/entries/main.ts"] +} diff --git a/tsconfig.base.json b/tsconfig.base.json index fb97cd8dd0..d92c4f645d 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -37,6 +37,9 @@ "@datadog/browser-rum-vue": ["./packages/rum-vue/src/entries/main"], "@datadog/browser-rum-vue/vue-router-v4": ["./packages/rum-vue/src/entries/vueRouter"], + "@datadog/browser-rum-angular": ["./packages/rum-angular/src/entries/main"], + "@datadog/browser-rum-angular/angular-router": ["./packages/rum-angular/src/entries/angularRouter"], + "@datadog/browser-rum-nextjs": ["./packages/rum-nextjs/src/entries/main"], "@datadog/browser-worker": ["./packages/worker/src/entries/main"] diff --git a/yarn.lock b/yarn.lock index 52215219a9..cce5ee9db2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34,6 +34,74 @@ __metadata: languageName: node linkType: hard +"@angular/common@npm:17.3.12": + version: 17.3.12 + resolution: "@angular/common@npm:17.3.12" + dependencies: + tslib: "npm:^2.3.0" + peerDependencies: + "@angular/core": 17.3.12 + rxjs: ^6.5.3 || ^7.4.0 + checksum: 10c0/f40d76081b04bacb3efd4389b0aa4dcfa742a4954e38a76a434dab1ae226b3192241077be5d0032cb011cc1625cb04043449cd5956f8ba44da99f8f7a3bbe83a + languageName: node + linkType: hard + +"@angular/compiler@npm:17.3.12": + version: 17.3.12 + resolution: "@angular/compiler@npm:17.3.12" + dependencies: + tslib: "npm:^2.3.0" + peerDependencies: + "@angular/core": 17.3.12 + peerDependenciesMeta: + "@angular/core": + optional: true + checksum: 10c0/e044b2cdd30deb43eda84b49ec0c9261d1745517927b0a7eacf0f1390ba9c8463a78945e10d888f209344085cde6d99495431f4c5401af412876e0e2141797b3 + languageName: node + linkType: hard + +"@angular/core@npm:17.3.12": + version: 17.3.12 + resolution: "@angular/core@npm:17.3.12" + dependencies: + tslib: "npm:^2.3.0" + peerDependencies: + rxjs: ^6.5.3 || ^7.4.0 + zone.js: ~0.14.0 + checksum: 10c0/0a2119daf7efdb81660c81dee1476b14e1f5456aee94e3f504cfbc1d98107dd7716374f9594837a5789d004b5c4b49aa7c553ef0dc87797cd581628bbfcd6858 + languageName: node + linkType: hard + +"@angular/platform-browser@npm:17.3.12": + version: 17.3.12 + resolution: "@angular/platform-browser@npm:17.3.12" + dependencies: + tslib: "npm:^2.3.0" + peerDependencies: + "@angular/animations": 17.3.12 + "@angular/common": 17.3.12 + "@angular/core": 17.3.12 + peerDependenciesMeta: + "@angular/animations": + optional: true + checksum: 10c0/ef0066f481920b4c778f7b342145790a6486f59faba4b88ad3216edb9e207f9a25d607d90e5b911654d092bf772261ab31de3c918a7b73fe8a507bdb6bfe54e1 + languageName: node + linkType: hard + +"@angular/router@npm:17.3.12": + version: 17.3.12 + resolution: "@angular/router@npm:17.3.12" + dependencies: + tslib: "npm:^2.3.0" + peerDependencies: + "@angular/common": 17.3.12 + "@angular/core": 17.3.12 + "@angular/platform-browser": 17.3.12 + rxjs: ^6.5.3 || ^7.4.0 + checksum: 10c0/b7d832804daa6c0777a2a09b4a53ae8c797e92e4224cc089bf8c05fb767b714c64e9feecaa5f0d4e3f72e86c487eddb23ebc0dc7a1a960b24e0ce96db99b4610 + languageName: node + linkType: hard + "@apidevtools/json-schema-ref-parser@npm:^11.5.5": version: 11.9.3 resolution: "@apidevtools/json-schema-ref-parser@npm:11.9.3" @@ -292,6 +360,41 @@ __metadata: languageName: unknown linkType: soft +"@datadog/browser-rum-angular@workspace:packages/rum-angular": + version: 0.0.0-use.local + resolution: "@datadog/browser-rum-angular@workspace:packages/rum-angular" + dependencies: + "@angular/common": "npm:17.3.12" + "@angular/compiler": "npm:17.3.12" + "@angular/core": "npm:17.3.12" + "@angular/platform-browser": "npm:17.3.12" + "@angular/router": "npm:17.3.12" + "@datadog/browser-core": "npm:6.32.0" + "@datadog/browser-rum-core": "npm:6.32.0" + rxjs: "npm:7.8.1" + tslib: "npm:2.8.1" + zone.js: "npm:0.14.10" + peerDependencies: + "@angular/common": ">=17.0.0" + "@angular/core": ">=17.0.0" + "@angular/router": ">=17.0.0" + rxjs: ">=7.0.0" + peerDependenciesMeta: + "@angular/common": + optional: true + "@angular/core": + optional: true + "@angular/router": + optional: true + "@datadog/browser-rum": + optional: true + "@datadog/browser-rum-slim": + optional: true + rxjs: + optional: true + languageName: unknown + linkType: soft + "@datadog/browser-rum-core@npm:6.32.0, @datadog/browser-rum-core@workspace:*, @datadog/browser-rum-core@workspace:packages/rum-core": version: 0.0.0-use.local resolution: "@datadog/browser-rum-core@workspace:packages/rum-core" @@ -10487,6 +10590,15 @@ __metadata: languageName: node linkType: hard +"rxjs@npm:7.8.1": + version: 7.8.1 + resolution: "rxjs@npm:7.8.1" + dependencies: + tslib: "npm:^2.1.0" + checksum: 10c0/3c49c1ecd66170b175c9cacf5cef67f8914dcbc7cd0162855538d365c83fea631167cacb644b3ce533b2ea0e9a4d0b12175186985f89d75abe73dbd8f7f06f68 + languageName: node + linkType: hard + "safe-array-concat@npm:^1.1.3": version: 1.1.3 resolution: "safe-array-concat@npm:1.1.3" @@ -11684,7 +11796,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.8.0": +"tslib@npm:2.8.1, tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.4.0, tslib@npm:^2.8.0": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 @@ -12991,3 +13103,10 @@ __metadata: checksum: 10c0/5718ec35e3c40b600316c5b4c5e4976f7fee68151bc8f8d90ec18a469be9571f072e1bbaace10f1e85cf8892ea12d90821b200e980ab46916a6166a4260a983c languageName: node linkType: hard + +"zone.js@npm:0.14.10": + version: 0.14.10 + resolution: "zone.js@npm:0.14.10" + checksum: 10c0/61283d152cb1eff899bae61621dccd572aa9f47e0c60c04b249bf86b43e3e4ba627bf6dba371b725023a4f302f39e554d7bf2d25bbf40c869c6c52f774b17e8b + languageName: node + linkType: hard