From fde974365fcb5a9cd5fcce862deed531da9c752a Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Tue, 26 May 2026 14:06:42 -0600 Subject: [PATCH 01/43] Upgrade Dynatrace RUM to 1.329+ with dual-flag version control - Add new inline scripts for all three environments (int, beta, prod) with agent version 10337260504112723 - Update dynatrace.ejs with frontier_snow_dynatraceNewRUM feature flag for gradual rollout - When new flag is ON: use 1.329+ version with enhanced data collection (owasp=1, uxrgce=1) - When new flag is OFF: use existing RUM version (backward compatible) - All three mechanisms supported: asyncCS-script, asyncCS-inline, global-cdn - New CDN URLs use SRI integrity hashes for added security - Existing frontier_snow_dynatraceRUM flag unchanged (still controls mechanism selection) Co-Authored-By: Claude Haiku 4.5 --- CHANGELOG-FRONTIER.md | 12 +++++++ .../layout/views/partials/dynatrace.ejs | 34 ++++++++++++++++--- .../partials/dynatrace/_inline_beta_new.ejs | 1 + .../partials/dynatrace/_inline_int_new.ejs | 1 + .../partials/dynatrace/_inline_prod_new.ejs | 1 + packages/react-scripts/package.json | 2 +- 6 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 packages/react-scripts/layout/views/partials/dynatrace/_inline_beta_new.ejs create mode 100644 packages/react-scripts/layout/views/partials/dynatrace/_inline_int_new.ejs create mode 100644 packages/react-scripts/layout/views/partials/dynatrace/_inline_prod_new.ejs diff --git a/CHANGELOG-FRONTIER.md b/CHANGELOG-FRONTIER.md index 6acb2fad696..8461faa9eb8 100644 --- a/CHANGELOG-FRONTIER.md +++ b/CHANGELOG-FRONTIER.md @@ -1,3 +1,15 @@ +## 8.17.0 + +- Upgrade Dynatrace RUM agent to version 1.329+ and enable new RUM experience + - Updated inline scripts for all three environments (int, beta, prod) to latest agent version (10337260504112723) + - Updated global CDN URLs to use SRI (Subresource Integrity) variant with integrity hashes for added security + - Added new feature flag `frontier_snow_dynatraceNewRUM` to control RUM version selection (gradual rollout support) + - All three loading mechanisms (asyncCS-script, asyncCS-inline, global-cdn) now support new RUM version + - Existing `frontier_snow_dynatraceRUM` flag still controls mechanism selection; new flag controls version + - New RUM experience includes enhanced data collection with owasp=1 and uxrgce=1 parameters + - When new flag is OFF: uses existing RUM version and URLs (backward compatible) + - When new flag is ON: uses new 1.329+ version with SRI integrity checks + ## 8.16.0 - Enable Jest CLI argument passing through test wrapper scripts (fr-test and conditional-test) diff --git a/packages/react-scripts/layout/views/partials/dynatrace.ejs b/packages/react-scripts/layout/views/partials/dynatrace.ejs index 6805ef274fc..edf1898c4b3 100644 --- a/packages/react-scripts/layout/views/partials/dynatrace.ejs +++ b/packages/react-scripts/layout/views/partials/dynatrace.ejs @@ -3,8 +3,11 @@ if (typeof getFeatureFlag !== 'undefined') { const dynatraceFlag = getFeatureFlag('frontier_snow_dynatraceRUM', {appName: process.env.APP_NAME}); + const dynatraceNewFlag = getFeatureFlag('frontier_snow_dynatraceNewRUM', {appName: process.env.APP_NAME}); const env = envType(); + const useNewRUM = dynatraceNewFlag.isOn; + // Old RUM version URLs const cdnUrls = { int: 'https://js-cdn.dynatrace.com/jstag/15c157a40ab/bf99293tkn/3faf90e849295814_complete.js', beta: 'https://js-cdn.dynatrace.com/jstag/15c157a40ab/bf99293tkn/c4242bb1eb216374_complete.js', @@ -17,12 +20,35 @@ if (typeof getFeatureFlag !== 'undefined') { prod: 'https://edge.fscdn.org/assets/components/hf/assets/js/monitoring/dynatrace-20221104-prod.min-44bb9345beec8a984c2fca40385b4f41.js' } + // New RUM 1.329+ version URLs (SRI variant with integrity hashes) + const cdnUrlsNew = { + int: 'https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA7NQVfghqrux_10337260504112723.js', + beta: 'https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA7NQVfghqrux_10337260504112723.js', + prod: 'https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA7NQVfghqrux_10337260504112723.js' + } + + const cdnIntegrityNew = 'sha256-6PrnNTp1/05LJTqnB9OOhKEVMId1XHk4Xa1x3cPF2Ec=' + + const selectedCdnUrls = useNewRUM ? cdnUrlsNew : cdnUrls; + if (dynatraceFlag.treatment === 'asyncCS-script') { %> - - <% } else if (dynatraceFlag.treatment === 'asyncCS-inline') { %> - <%- include(`./partials/dynatrace/_inline_${env}`) %> + <% if (useNewRUM) { %> + + <% } else { %> + + <% } %> + <% } else if (dynatraceFlag.treatment === 'asyncCS-inline') { %> + <% if (useNewRUM) { %> + <%- include(`./partials/dynatrace/_inline_${env}_new`) %> + <% } else { %> + <%- include(`./partials/dynatrace/_inline_${env}`) %> + <% } %> <% } else if (dynatraceFlag.treatment === 'global-cdn') { %> - + <% if (useNewRUM) { %> + + <% } else { %> + + <% } %> <% } %> <% } %> diff --git a/packages/react-scripts/layout/views/partials/dynatrace/_inline_beta_new.ejs b/packages/react-scripts/layout/views/partials/dynatrace/_inline_beta_new.ejs new file mode 100644 index 00000000000..f00f3f968c3 --- /dev/null +++ b/packages/react-scripts/layout/views/partials/dynatrace/_inline_beta_new.ejs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/react-scripts/layout/views/partials/dynatrace/_inline_int_new.ejs b/packages/react-scripts/layout/views/partials/dynatrace/_inline_int_new.ejs new file mode 100644 index 00000000000..c66e528f61f --- /dev/null +++ b/packages/react-scripts/layout/views/partials/dynatrace/_inline_int_new.ejs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/react-scripts/layout/views/partials/dynatrace/_inline_prod_new.ejs b/packages/react-scripts/layout/views/partials/dynatrace/_inline_prod_new.ejs new file mode 100644 index 00000000000..dd5138761d1 --- /dev/null +++ b/packages/react-scripts/layout/views/partials/dynatrace/_inline_prod_new.ejs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index 97ddd921703..aaff449610e 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -1,6 +1,6 @@ { "name": "@fs/react-scripts", - "version": "8.16.0", + "version": "8.17.0", "upstreamVersion": "5.0.1", "description": "Configuration and scripts for Create React App.", "repository": { From 605d441ed78897396c8339eb08cb845b1a435983 Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Tue, 26 May 2026 14:07:26 -0600 Subject: [PATCH 02/43] Add Dynatrace RUM automation and update documentation - Add fetch-dynatrace-scripts.js helper for automated future RUM updates - Fetches latest scripts from Dynatrace API - Auto-writes inline scripts to _inline_*_new.ejs files - Displays CDN URLs and integrity hashes in copy-paste format - Add DYNATRACE_RUM_UPDATE.md with complete update procedures - Documents API setup, entity IDs, and testing procedures - Enables maintainers to automate RUM upgrades in the future Co-Authored-By: Claude Haiku 4.5 --- .../layout/views/partials/dynatrace.ejs | 12 +- .../partials/dynatrace/_inline_beta_new.ejs | 2 +- .../partials/dynatrace/_inline_int_new.ejs | 2 +- .../partials/dynatrace/_inline_prod_new.ejs | 2 +- .../scripts/DYNATRACE_RUM_UPDATE.md | 184 +++++++++++++ .../scripts/fetch-dynatrace-scripts.js | 241 ++++++++++++++++++ 6 files changed, 436 insertions(+), 7 deletions(-) create mode 100644 packages/react-scripts/scripts/DYNATRACE_RUM_UPDATE.md create mode 100644 packages/react-scripts/scripts/fetch-dynatrace-scripts.js diff --git a/packages/react-scripts/layout/views/partials/dynatrace.ejs b/packages/react-scripts/layout/views/partials/dynatrace.ejs index edf1898c4b3..01bdf3e4db3 100644 --- a/packages/react-scripts/layout/views/partials/dynatrace.ejs +++ b/packages/react-scripts/layout/views/partials/dynatrace.ejs @@ -22,18 +22,22 @@ if (typeof getFeatureFlag !== 'undefined') { // New RUM 1.329+ version URLs (SRI variant with integrity hashes) const cdnUrlsNew = { - int: 'https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA7NQVfghqrux_10337260504112723.js', + int: 'https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA15789NPQRTUVXfghqrux_10337260504112723.js', beta: 'https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA7NQVfghqrux_10337260504112723.js', prod: 'https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA7NQVfghqrux_10337260504112723.js' } - const cdnIntegrityNew = 'sha256-6PrnNTp1/05LJTqnB9OOhKEVMId1XHk4Xa1x3cPF2Ec=' + const cdnIntegrityNew = { + int: 'sha256-KHFYWCusB88n37My9yKVX/ES9BX6Wy+k74hsDScU5jY=', + beta: 'sha256-6PrnNTp1/05LJTqnB9OOhKEVMId1XHk4Xa1x3cPF2Ec=', + prod: 'sha256-6PrnNTp1/05LJTqnB9OOhKEVMId1XHk4Xa1x3cPF2Ec=' + } const selectedCdnUrls = useNewRUM ? cdnUrlsNew : cdnUrls; if (dynatraceFlag.treatment === 'asyncCS-script') { %> <% if (useNewRUM) { %> - + <% } else { %> <% } %> @@ -45,7 +49,7 @@ if (typeof getFeatureFlag !== 'undefined') { <% } %> <% } else if (dynatraceFlag.treatment === 'global-cdn') { %> <% if (useNewRUM) { %> - + <% } else { %> <% } %> diff --git a/packages/react-scripts/layout/views/partials/dynatrace/_inline_beta_new.ejs b/packages/react-scripts/layout/views/partials/dynatrace/_inline_beta_new.ejs index f00f3f968c3..0ea4a7501d1 100644 --- a/packages/react-scripts/layout/views/partials/dynatrace/_inline_beta_new.ejs +++ b/packages/react-scripts/layout/views/partials/dynatrace/_inline_beta_new.ejs @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/react-scripts/layout/views/partials/dynatrace/_inline_int_new.ejs b/packages/react-scripts/layout/views/partials/dynatrace/_inline_int_new.ejs index c66e528f61f..b8cfb504233 100644 --- a/packages/react-scripts/layout/views/partials/dynatrace/_inline_int_new.ejs +++ b/packages/react-scripts/layout/views/partials/dynatrace/_inline_int_new.ejs @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/react-scripts/layout/views/partials/dynatrace/_inline_prod_new.ejs b/packages/react-scripts/layout/views/partials/dynatrace/_inline_prod_new.ejs index dd5138761d1..d3e7c6e2d1f 100644 --- a/packages/react-scripts/layout/views/partials/dynatrace/_inline_prod_new.ejs +++ b/packages/react-scripts/layout/views/partials/dynatrace/_inline_prod_new.ejs @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/react-scripts/scripts/DYNATRACE_RUM_UPDATE.md b/packages/react-scripts/scripts/DYNATRACE_RUM_UPDATE.md new file mode 100644 index 00000000000..442e573c4f8 --- /dev/null +++ b/packages/react-scripts/scripts/DYNATRACE_RUM_UPDATE.md @@ -0,0 +1,184 @@ +# Updating Dynatrace RUM Scripts + +This guide explains how to update the Dynatrace RUM agent to a new version using the automated fetch script. + +## Quick Start + +```bash +export DYNATRACE_API_TOKEN="your-api-token" +node packages/react-scripts/scripts/fetch-dynatrace-scripts.js +``` + +The script will fetch the latest RUM scripts and CDN URLs for all three environments (int, beta, prod). + +## Prerequisites + +### 1. Get a Dynatrace API Token + +1. Log into your Dynatrace environment: https://bjm35087.live.dynatrace.com +2. Go to Account β†’ Access Tokens +3. Create a new API token with these scopes: + - **Read RUM manual insertion tags** (API v2) - for fetching scripts + - **Real user monitoring JavaScript tag management** (API v1) - optional, for additional RUM data + +### 2. Know Your Entity IDs + +The script uses these default entity IDs: +- **Int**: `APPLICATION-3FAF90E849295814` +- **Beta**: `APPLICATION-C4242BB1EB216374` +- **Prod**: `APPLICATION-A8E5EDD77F861ACE` + +To find entity IDs yourself: +1. Log into Dynatrace +2. Go to Applications β†’ select your app β†’ Settings +3. The entity ID is shown in the URL or settings page + +## How to Use + +### Step 1: Fetch the Latest Scripts + +```bash +export DYNATRACE_API_TOKEN="your-token-here" +node packages/react-scripts/scripts/fetch-dynatrace-scripts.js +``` + +The script will: +1. βœ… **Automatically write inline scripts** to `_inline_*_new.ejs` files +2. πŸ“Œ **Display CDN URLs** in copy-paste format +3. πŸ”’ **Display integrity hashes** for SRI verification + +### Step 2: Update CDN URLs in dynatrace.ejs + +The script output will display CDN URLs in this format: + +``` +CDN URLs for dynatrace.ejs (cdnUrlsNew object): + + int: 'https://js-cdn.dynatrace.com/...', + beta: 'https://js-cdn.dynatrace.com/...', + prod: 'https://js-cdn.dynatrace.com/...', + +Integrity hash for all environments: + cdnIntegrityNew = 'sha256-...' +``` + +Update these values in `packages/react-scripts/layout/views/partials/dynatrace.ejs`: + +```javascript +const cdnUrlsNew = { + int: '...', // Copy from script output + beta: '...', // Copy from script output + prod: '...' // Copy from script output +} + +const cdnIntegrityNew = '...' // Copy from script output +``` + +### Step 3: Test All Mechanisms + +The existing feature flag logic supports gradual rollout (see [dynatrace.ejs](./partials/dynatrace.ejs)): + +- **`frontier_snow_dynatraceRUM`**: Selects the loading mechanism (asyncCS-script, asyncCS-inline, or global-cdn) +- **`frontier_snow_dynatraceNewRUM`**: Selects the RUM version (old or new) + +Test each mechanism: + +```bash +# For asyncCS-script (uses Edge CDN) +# - Set frontier_snow_dynatraceRUM to "asyncCS-script" +# - Verify script loads from edge.fscdn.org or Dynatrace CDN + +# For asyncCS-inline (embedded inline) +# - Set frontier_snow_dynatraceRUM to "asyncCS-inline" +# - Verify script is embedded in HTML + +# For global-cdn (external from Dynatrace CDN) +# - Set frontier_snow_dynatraceRUM to "global-cdn" +# - Verify script loads from js-cdn.dynatrace.com +``` + +### Step 4: Commit and Release + +```bash +# Stage all changes +git add packages/react-scripts/layout/views/partials/dynatrace.ejs +git add packages/react-scripts/layout/views/partials/dynatrace/_inline_*_new.ejs + +# Commit with descriptive message +git commit -m "Update Dynatrace RUM to latest version + +- Update inline scripts for int, beta, prod environments (via fetch-dynatrace-scripts.js) +- Update CDN URLs with latest SRI integrity hashes +- New RUM version includes enhanced data collection (owasp=1, uxrgce=1)" + +# Bump version in package.json (usually minor bump) +# Example: 8.16.0 β†’ 8.17.0 +sed -i '' 's/"version": "8\.16\.0"/"version": "8.17.0"/' packages/react-scripts/package.json + +# Update CHANGELOG-FRONTIER.md +# Add entry at the top with the new version and upgrade details + +# Commit version and changelog +git add packages/react-scripts/package.json CHANGELOG-FRONTIER.md +git commit -m "Bump react-scripts to 8.17.0 with Dynatrace RUM upgrade" + +# Push when ready +git push origin dynatrace-rum-upgrade +``` + +## Understanding the Output + +The API returns complete script tags like: + +```html + +``` + +Key parts: +- **`src`**: The CDN URL for the external script +- **`data-dtconfig`**: Configuration string (includes app ID, parameters, etc.) +- **`integrity`**: SRI hash for script verification +- **app ID**: The environment-specific application identifier (e.g., `app=3faf90e849295814`) + +## Files Modified During Update + +``` +packages/react-scripts/layout/views/partials/dynatrace.ejs +packages/react-scripts/layout/views/partials/dynatrace/_inline_int_new.ejs +packages/react-scripts/layout/views/partials/dynatrace/_inline_beta_new.ejs +packages/react-scripts/layout/views/partials/dynatrace/_inline_prod_new.ejs +packages/react-scripts/package.json (version bump) +CHANGELOG-FRONTIER.md (add entry) +``` + +## Troubleshooting + +**API returns 401 Unauthorized** +- Check that `DYNATRACE_API_TOKEN` environment variable is set +- Verify the token has the correct scopes (RUM manual insertion tags read) + +**API returns 400 Bad Request** +- Ensure entity IDs include the `APPLICATION-` prefix +- Entity IDs must be uppercase + +**API returns 404 Not Found** +- Verify the API endpoint URL is correct: `/api/v2/rum/oneAgentJavaScriptTagWithSri/{entityId}` +- Check that you're using the `.live.dynatrace.com` domain, not `.apps.dynatrace.com` + +## Related Documentation + +- [Dynatrace RUM API Documentation](https://docs.dynatrace.com/docs/dynatrace-api/environment-api/rum/rum-manual-insertion-tags) +- [dynatrace.ejs](./partials/dynatrace.ejs) - Main RUM configuration file +- [fetch-dynatrace-scripts.js](./fetch-dynatrace-scripts.js) - The fetch script itself + +## Version History + +- **8.17.0**: First release with dual-flag support (old vs new RUM version) + - Introduced `frontier_snow_dynatraceNewRUM` feature flag for gradual rollout + - Updated to RUM 1.329+ with enhanced data collection (owasp=1, uxrgce=1) + - Added SRI integrity hash support diff --git a/packages/react-scripts/scripts/fetch-dynatrace-scripts.js b/packages/react-scripts/scripts/fetch-dynatrace-scripts.js new file mode 100644 index 00000000000..7c889b3342c --- /dev/null +++ b/packages/react-scripts/scripts/fetch-dynatrace-scripts.js @@ -0,0 +1,241 @@ +#!/usr/bin/env node +/** + * Dynatrace RUM Script Fetcher + * + * Fetches the latest RUM inline scripts and tags from Dynatrace using the API. + * Automatically updates inline script files and displays CDN URLs for updating dynatrace.ejs. + * Requires Dynatrace API token with RUM manual insertion tags read scope. + * + * IMPORTANT: This API may not be available on all Dynatrace instances or configurations. + * If the API is not accessible, fetch scripts manually from: + * Dynatrace UI β†’ Settings β†’ Real User Monitoring β†’ Managed JavaScript + * (copy the inline script and CDN URL for each environment) + * + * Usage: + * node fetch-dynatrace-scripts.js # reads token from macOS keychain + * DYNATRACE_API_TOKEN="xxx" node fetch-dynatrace-scripts.js # override for one run + * + * Token setup (one-time): + * security add-generic-password -a dynatrace-rum-fetch -s dynatrace-api-token -w + * + * Configuration: + * - Token is auto-read from keychain; set DYNATRACE_API_TOKEN env var to override + * - Update DYNATRACE_ENVIRONMENT_URL and ENTITY_IDS below with your values + * + * API Documentation: + * https://docs.dynatrace.com/docs/dynatrace-api/environment-api/rum/rum-manual-insertion-tags + */ + +const https = require('https'); +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +const KEYCHAIN_ACCOUNT = 'dynatrace-rum-fetch'; +const KEYCHAIN_SERVICE = 'dynatrace-api-token'; + +function getTokenFromKeychain() { + try { + const token = execSync( + `security find-generic-password -a "${KEYCHAIN_ACCOUNT}" -s "${KEYCHAIN_SERVICE}" -w`, + { stdio: ['pipe', 'pipe', 'pipe'] } + ).toString().trim(); + return token || null; + } catch { + return null; + } +} + +function printKeychainSetupInstructions() { + console.error(` +ERROR: No Dynatrace API token found. + +To store your token in the macOS keychain (one-time setup): + + security add-generic-password \\ + -a "${KEYCHAIN_ACCOUNT}" \\ + -s "${KEYCHAIN_SERVICE}" \\ + -w + +You'll be prompted to enter the token value (it won't appear in shell history). + +To get a token: + 1. Log into https://bjm35087.live.dynatrace.com + 2. Go to Account β†’ Access Tokens β†’ Generate new token + 3. Add scope: "Read RUM manual insertion tags" + +To update an existing token: + security delete-generic-password -a "${KEYCHAIN_ACCOUNT}" -s "${KEYCHAIN_SERVICE}" + security add-generic-password -a "${KEYCHAIN_ACCOUNT}" -s "${KEYCHAIN_SERVICE}" -w + +Or override for a single run: + DYNATRACE_API_TOKEN="dt0c01.xxx" node fetch-dynatrace-scripts.js +`); +} + +// Configuration +const DYNATRACE_ENVIRONMENT_URL = process.env.DYNATRACE_ENV_URL || "https://bjm35087.live.dynatrace.com"; +const DYNATRACE_API_TOKEN = process.env.DYNATRACE_API_TOKEN || getTokenFromKeychain(); + +// Entity IDs for your RUM applications in each environment +// Get from Dynatrace UI: Applications β†’ select app β†’ Settings β†’ note the entity ID +const ENTITY_IDS = { + int: process.env.INT_ENTITY_ID || "APPLICATION-3FAF90E849295814", + beta: process.env.BETA_ENTITY_ID || "APPLICATION-C4242BB1EB216374", + prod: process.env.PROD_ENTITY_ID || "APPLICATION-A8E5EDD77F861ACE", +}; + +function parseUrl(urlString) { + const url = new URL(urlString); + return { + hostname: url.hostname, + path: url.pathname.replace(/\/$/, ''), + }; +} + +async function makeRequest(hostname, path) { + return new Promise((resolve, reject) => { + const options = { + hostname, + path, + method: 'GET', + headers: { + 'Authorization': `Api-Token ${DYNATRACE_API_TOKEN}`, + 'Content-Type': 'application/json', + }, + }; + + https.request(options, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + if (res.statusCode >= 200 && res.statusCode < 300) { + resolve(data); + } else { + reject(`HTTP ${res.statusCode}: ${data}`); + } + }); + }).on('error', reject).end(); + }); +} + +function updateDynatraceEjs(results) { + const ejsPath = path.join(__dirname, '../layout/views/partials/dynatrace.ejs'); + let content = fs.readFileSync(ejsPath, 'utf8'); + + // Build replacement cdnUrlsNew block + const urlLines = Object.entries(results).map(([env, data]) => { + const src = data.completeTag.match(/src="([^"]+)"/)?.[1] || ''; + return ` ${env}: '${src}'`; + }).join(',\n'); + content = content.replace( + /const cdnUrlsNew = \{[^}]+\}/s, + `const cdnUrlsNew = {\n${urlLines}\n }` + ); + + // Build replacement cdnIntegrityNew block (per-env object) + const integrityLines = Object.entries(results).map(([env, data]) => { + const hash = data.completeTag.match(/integrity="([^"]+)"/)?.[1] || ''; + return ` ${env}: '${hash}'`; + }).join(',\n'); + content = content.replace( + /const cdnIntegrityNew = (\{[^}]+\}|'[^']+')/s, + `const cdnIntegrityNew = {\n${integrityLines}\n }` + ); + + fs.writeFileSync(ejsPath, content, 'utf8'); + console.log(" βœ… Updated dynatrace.ejs (cdnUrlsNew + cdnIntegrityNew)"); +} + +async function fetchScripts() { + if (!DYNATRACE_API_TOKEN) { + printKeychainSetupInstructions(); + process.exit(1); + } + + const missingIds = Object.entries(ENTITY_IDS) + .filter(([, id]) => !id) + .map(([env]) => env); + + if (missingIds.length > 0) { + console.error(`ERROR: Missing entity IDs for: ${missingIds.join(", ")}`); + console.error("Set environment variables: INT_ENTITY_ID, BETA_ENTITY_ID, PROD_ENTITY_ID"); + process.exit(1); + } + + try { + const { hostname, path: basePath } = parseUrl(DYNATRACE_ENVIRONMENT_URL); + console.log("Fetching Dynatrace RUM scripts...\n"); + + const results = {}; + + for (const [env, entityId] of Object.entries(ENTITY_IDS)) { + console.log(`πŸ“₯ Fetching ${env.toUpperCase()} environment (entity: ${entityId})...`); + + try { + // Fetch OneAgent JavaScript tag with SRI (includes inline script and complete tag) + const path = `${basePath}/api/v2/rum/oneAgentJavaScriptTagWithSri/${entityId}`; + const response = await makeRequest(hostname, path); + + results[env] = { + completeTag: response.trim(), + }; + + console.log(` βœ… Complete tag fetched (${response.length} chars)`); + + // Extract CDN URL from complete tag + const srcMatch = response.match(/src="([^"]+)"/); + if (srcMatch) { + console.log(` πŸ“Œ CDN URL: ${srcMatch[1]}`); + } + + // Extract integrity hash if present + const integrityMatch = response.match(/integrity="([^"]+)"/); + if (integrityMatch) { + console.log(` πŸ”’ Integrity: ${integrityMatch[1]}`); + } + + console.log(""); + } catch (error) { + console.error(` ❌ Failed to fetch ${env}: ${error}\n`); + throw error; + } + } + + // Write inline scripts to files + console.log("\nπŸ“ Writing inline scripts to files...\n"); + + const inlineScriptDir = path.join(__dirname, '../layout/views/partials/dynatrace'); + + for (const [env, data] of Object.entries(results)) { + const inlineFilePath = path.join(inlineScriptDir, `_inline_${env}_new.ejs`); + + try { + fs.writeFileSync(inlineFilePath, data.completeTag, 'utf8'); + console.log(` βœ… Wrote ${path.basename(inlineFilePath)}`); + } catch (error) { + console.error(` ❌ Failed to write ${env} inline script: ${error.message}`); + } + } + + // Update dynatrace.ejs with new CDN URLs and per-env integrity hashes + updateDynatraceEjs(results); + + console.log("\n✨ Scripts fetched and all files updated successfully!"); + console.log("\nNext steps:"); + console.log("1. βœ… Inline scripts written to _inline_*_new.ejs files"); + console.log("2. βœ… dynatrace.ejs updated with new CDN URLs and integrity hashes"); + console.log("3. Test all three mechanisms (asyncCS-script, asyncCS-inline, global-cdn)"); + + return results; + } catch (error) { + console.error("Fatal error:", error); + process.exit(1); + } +} + +if (require.main === module) { + fetchScripts(); +} + +module.exports = { fetchScripts }; From b44b8f7d955a80e3592265a7e63e06d4a264d38f Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Mon, 1 Jun 2026 03:18:19 -0600 Subject: [PATCH 03/43] Add RUM loading mechanism telemetry for performance comparison Report the active loading mechanism (asyncCS-script, asyncCS-inline, or global-cdn) as a custom metric to Dynatrace. This enables direct DQL queries to correlate loading mechanism with Core Web Vitals performance (LCP, CLS, page load time). - Add window.dtrum.reportCustomMetric call to emit rum_loading_mechanism value - Bump version to 8.17.0-alpha.1 for telemetry feature Co-Authored-By: Claude Haiku 4.5 --- packages/react-scripts/layout/views/partials/dynatrace.ejs | 7 +++++++ packages/react-scripts/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/react-scripts/layout/views/partials/dynatrace.ejs b/packages/react-scripts/layout/views/partials/dynatrace.ejs index 01bdf3e4db3..de9e2abe9c4 100644 --- a/packages/react-scripts/layout/views/partials/dynatrace.ejs +++ b/packages/react-scripts/layout/views/partials/dynatrace.ejs @@ -55,4 +55,11 @@ if (typeof getFeatureFlag !== 'undefined') { <% } %> <% } %> + + + <% } %> diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index aaff449610e..2560efe04e4 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -1,6 +1,6 @@ { "name": "@fs/react-scripts", - "version": "8.17.0", + "version": "8.17.0-alpha.1", "upstreamVersion": "5.0.1", "description": "Configuration and scripts for Create React App.", "repository": { From 212832de472f9719008562fb62b1351764b5caaf Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Mon, 1 Jun 2026 03:33:17 -0600 Subject: [PATCH 04/43] Fix Dynatrace beacon reporting by preserving data-dtconfig attribute The RUM agent needs the data-dtconfig attribute (which includes reportUrl) to send beacons directly to the Dynatrace tenant instead of back through the app. Previously, only src and integrity were being extracted from the Dynatrace API response. Changes: - Extract and preserve data-dtconfig from complete script tags - Add cdnConfigNew object in dynatrace.ejs with per-environment configs - Apply data-dtconfig attribute to asyncCS-script and global-cdn mechanisms - Print config extraction confirmation in fetch script This fixes the 405 POST errors by ensuring RUM beacons are routed to the correct Dynatrace backend endpoint (reportUrl), not to the app. Co-Authored-By: Claude Haiku 4.5 --- .../layout/views/partials/dynatrace.ejs | 11 ++++++++-- .../scripts/fetch-dynatrace-scripts.js | 21 ++++++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/react-scripts/layout/views/partials/dynatrace.ejs b/packages/react-scripts/layout/views/partials/dynatrace.ejs index de9e2abe9c4..6045d10e4cc 100644 --- a/packages/react-scripts/layout/views/partials/dynatrace.ejs +++ b/packages/react-scripts/layout/views/partials/dynatrace.ejs @@ -33,11 +33,18 @@ if (typeof getFeatureFlag !== 'undefined') { prod: 'sha256-6PrnNTp1/05LJTqnB9OOhKEVMId1XHk4Xa1x3cPF2Ec=' } + const cdnConfigNew = { + int: '', + beta: '', + prod: '' + } + const selectedCdnUrls = useNewRUM ? cdnUrlsNew : cdnUrls; + const selectedCdnConfig = useNewRUM ? cdnConfigNew : {}; if (dynatraceFlag.treatment === 'asyncCS-script') { %> <% if (useNewRUM) { %> - + <% } else { %> <% } %> @@ -49,7 +56,7 @@ if (typeof getFeatureFlag !== 'undefined') { <% } %> <% } else if (dynatraceFlag.treatment === 'global-cdn') { %> <% if (useNewRUM) { %> - + <% } else { %> <% } %> diff --git a/packages/react-scripts/scripts/fetch-dynatrace-scripts.js b/packages/react-scripts/scripts/fetch-dynatrace-scripts.js index 7c889b3342c..da0d39dd819 100644 --- a/packages/react-scripts/scripts/fetch-dynatrace-scripts.js +++ b/packages/react-scripts/scripts/fetch-dynatrace-scripts.js @@ -143,8 +143,21 @@ function updateDynatraceEjs(results) { `const cdnIntegrityNew = {\n${integrityLines}\n }` ); + // Build replacement cdnConfigNew block (per-env object with data-dtconfig) + const configLines = Object.entries(results).map(([env, data]) => { + const config = data.completeTag.match(/data-dtconfig="([^"]+)"/)?.[1] || ''; + return ` ${env}: '${config}'`; + }).join(',\n'); + content = content.replace( + /const cdnConfigNew = (\{[^}]+\}|'[^']+')/s, + `const cdnConfigNew = {\n${configLines}\n }` + ) || content.replace( + /const cdnIntegrityNew = \{[^}]+\}/s, + match => `${match}\n\n const cdnConfigNew = {\n${configLines}\n }` + ); + fs.writeFileSync(ejsPath, content, 'utf8'); - console.log(" βœ… Updated dynatrace.ejs (cdnUrlsNew + cdnIntegrityNew)"); + console.log(" βœ… Updated dynatrace.ejs (cdnUrlsNew + cdnIntegrityNew + cdnConfigNew)"); } async function fetchScripts() { @@ -195,6 +208,12 @@ async function fetchScripts() { console.log(` πŸ”’ Integrity: ${integrityMatch[1]}`); } + // Extract data-dtconfig if present + const configMatch = response.match(/data-dtconfig="([^"]+)"/); + if (configMatch) { + console.log(` βš™οΈ Config includes reportUrl for direct beacon delivery`); + } + console.log(""); } catch (error) { console.error(` ❌ Failed to fetch ${env}: ${error}\n`); From 0bfcd58d99504c4af426077d063fff349e72d942 Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Mon, 1 Jun 2026 03:35:28 -0600 Subject: [PATCH 05/43] Bump react-scripts to 8.17.0-alpha.2 Includes full data-dtconfig with proper reportUrl for direct beacon delivery to Dynatrace tenant. Co-Authored-By: Claude Haiku 4.5 --- packages/react-scripts/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index 2560efe04e4..6940d1a9846 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -1,6 +1,6 @@ { "name": "@fs/react-scripts", - "version": "8.17.0-alpha.1", + "version": "8.17.0-alpha.2", "upstreamVersion": "5.0.1", "description": "Configuration and scripts for Create React App.", "repository": { From 6c51091343bf0b8bbc0c7479c7794a5165c9ea47 Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Mon, 1 Jun 2026 04:43:27 -0600 Subject: [PATCH 06/43] Use inline script approach for new RUM with proper JSON config The new RUM script expects config in JSON format (data-config, data-appconfig, data-envconfig) which the fetch script provides in the inline script files. External script tags cannot reliably read data-* attributes, so we use the inline approach for both asyncCS-script and global-cdn mechanisms when new RUM is enabled. This fixes the initialization error: 'DT_RUM will not initialize: None of the supported configuration sources contain the required properties [revision, environmentId, app]' Co-Authored-By: Claude Haiku 4.5 --- .../layout/views/partials/dynatrace.ejs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/react-scripts/layout/views/partials/dynatrace.ejs b/packages/react-scripts/layout/views/partials/dynatrace.ejs index 6045d10e4cc..ece7859efd4 100644 --- a/packages/react-scripts/layout/views/partials/dynatrace.ejs +++ b/packages/react-scripts/layout/views/partials/dynatrace.ejs @@ -34,9 +34,9 @@ if (typeof getFeatureFlag !== 'undefined') { } const cdnConfigNew = { - int: '', - beta: '', - prod: '' + int: 'app=3faf90e849295814|cors=1|owasp=1|featureHash=ICA15789NPQRTUVXfghqrux|msl=153600|srsr=10000|nsfnv=1|reportUrl=https://bf99293tkn.bf.dynatrace.com/bf|srvr=%5C%2Fidentity%5C%2Fsettings|rdnt=2|uxrgce=1|cuc=t0rtn87t|srms=2,1,0,0%2Ftextarea%2Cinput%2Cselect%2Coption;0%2Fdatalist;0%2Fform%20button;0%2F%5Bdata-dtrum-input%5D;0%2F.data-dtrum-input;1%2F%5Edata%28%28%5C-.%2B%24%29%7C%24%29|mel=100000|dpvc=1|md=mdcc1=cfs_anid,mdcc2=bFS.User.profile.cisId,mdcc3=bs.pageName,mdcc4=bFS.attrs.anonId,mdcc5=bFS.attrs.country,mdcc6=bFS.attrs.cisId|lastModification=1780298395720|mdp=mdcc4,mdcc5|tp=500,50,0|srbbv=2|agentUri=https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA15789NPQRTUVXfghqrux_10337260504112723.js', + beta: 'app=c4242bb1eb216374|cors=1|owasp=1|featureHash=ICA7NQVfghqrux|msl=153600|srsr=10000|nsfnv=1|reportUrl=https://bf99293tkn.bf.dynatrace.com/bf|srvr=%5C%2Fidentity%5C%2Fsettings|rdnt=2|uxrgce=1|cuc=t0rtn87t|srms=2,1,0,0%2Ftextarea%2Cinput%2Cselect%2Coption;0%2Fdatalist;0%2Fform%20button;0%2F%5Bdata-dtrum-input%5D;0%2F.data-dtrum-input;1%2F%5Edata%28%28%5C-.%2B%24%29%7C%24%29|mel=100000|dpvc=1|md=mdcc1=cfs_anid,mdcc2=bFS.attrs.cisId,mdcc3=bFS.User.profile.cisId,mdcc4=fx-ratelimit-remaining,mdcc5=bs.campaign,mdcc6=bs.channel,mdcc7=bs.pageName,mdcc8=bs.pageURL,mdcc9=bs.pageType,mdcc10=bs.visitorID|lastModification=1780298395720|tp=500,50,0|srbbv=2|agentUri=https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA7NQVfghqrux_10337260504112723.js', + prod: 'app=a8e5edd77f861ace|cors=1|owasp=1|featureHash=ICA7NQVfghqrux|msl=153600|srsr=10000|vcx=1500|nsfnv=1|reportUrl=https://bf99293tkn.bf.dynatrace.com/bf|srvr=%5C%2Fidentity%5C%2Fsettings|rdnt=2|uxrgce=1|vcit=8000|cuc=t0rtn87t|srms=2,2,1,|mdl=mdcc11=20|mel=100000|dpvc=1|md=mdcc1=cfs_anid,mdcc2=bFS.attrs.cisId,mdcc3=bFS.User.profile.cisId,mdcc4=bs.pageName,mdcc5=bs.pageType,mdcc6=bs.pageURL,mdcc7=bs.visitorID,mdcc8=bs.campaign,mdcc9=bs.channel,mdcc10=fx-ratelimit-remaining,mdcc11=bdocument.referrer,mdcc12=dutm_campaign,mdcc13=dutm_source,mdcc14=dutm_medium|lastModification=1780298395720|tp=500,50,0|srbbv=2|agentUri=https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA7NQVfghqrux_10337260504112723.js' } const selectedCdnUrls = useNewRUM ? cdnUrlsNew : cdnUrls; @@ -44,7 +44,8 @@ if (typeof getFeatureFlag !== 'undefined') { if (dynatraceFlag.treatment === 'asyncCS-script') { %> <% if (useNewRUM) { %> - + + <%- include(`./partials/dynatrace/_inline_${env}_new`) %> <% } else { %> <% } %> @@ -56,7 +57,8 @@ if (typeof getFeatureFlag !== 'undefined') { <% } %> <% } else if (dynatraceFlag.treatment === 'global-cdn') { %> <% if (useNewRUM) { %> - + + <%- include(`./partials/dynatrace/_inline_${env}_new`) %> <% } else { %> <% } %> From 2d6cf85edd21b9f56fcd5dba786c71d2e40db3c5 Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Mon, 1 Jun 2026 04:45:06 -0600 Subject: [PATCH 07/43] Update package-lock.json and fetch script permissions for alpha.2 release - Reflect version bump to 8.17.0-alpha.2 in package-lock.json - Make fetch-dynatrace-scripts.js executable Co-Authored-By: Claude Haiku 4.5 --- package-lock.json | 2 +- packages/react-scripts/scripts/fetch-dynatrace-scripts.js | 0 2 files changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 packages/react-scripts/scripts/fetch-dynatrace-scripts.js diff --git a/package-lock.json b/package-lock.json index f52f123e61e..f3a5d1893fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43779,7 +43779,7 @@ }, "packages/react-scripts": { "name": "@fs/react-scripts", - "version": "8.16.0", + "version": "8.17.0-alpha.2", "license": "MIT", "dependencies": { "@babel/core": "^7.16.0", diff --git a/packages/react-scripts/scripts/fetch-dynatrace-scripts.js b/packages/react-scripts/scripts/fetch-dynatrace-scripts.js old mode 100644 new mode 100755 From 30f35bf3b81f302f26b332914f8a768268d5f306 Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Mon, 1 Jun 2026 04:46:01 -0600 Subject: [PATCH 08/43] Bump react-scripts to 8.17.0-alpha.3 Fixes RUM initialization by using inline script approach with proper JSON config format for new RUM. Co-Authored-By: Claude Haiku 4.5 --- packages/react-scripts/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index 6940d1a9846..9ef50740ee8 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -1,6 +1,6 @@ { "name": "@fs/react-scripts", - "version": "8.17.0-alpha.2", + "version": "8.17.0-alpha.3", "upstreamVersion": "5.0.1", "description": "Configuration and scripts for Create React App.", "repository": { From f02d9c41ecef8abb7cea37f5404eae355b38f49b Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Mon, 1 Jun 2026 04:50:00 -0600 Subject: [PATCH 09/43] Fix RUM loading mechanism telemetry timing Report rum_loading_mechanism metric immediately after each RUM script loads, rather than at the end of page. This ensures the custom metric is captured in the first page summary event before any beacons are sent. Co-Authored-By: Claude Haiku 4.5 --- .../layout/views/partials/dynatrace.ejs | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/packages/react-scripts/layout/views/partials/dynatrace.ejs b/packages/react-scripts/layout/views/partials/dynatrace.ejs index ece7859efd4..a23c33df052 100644 --- a/packages/react-scripts/layout/views/partials/dynatrace.ejs +++ b/packages/react-scripts/layout/views/partials/dynatrace.ejs @@ -46,29 +46,58 @@ if (typeof getFeatureFlag !== 'undefined') { <% if (useNewRUM) { %> <%- include(`./partials/dynatrace/_inline_${env}_new`) %> + + <% } else { %> + + <% } %> <% } else if (dynatraceFlag.treatment === 'asyncCS-inline') { %> <% if (useNewRUM) { %> <%- include(`./partials/dynatrace/_inline_${env}_new`) %> + + <% } else { %> <%- include(`./partials/dynatrace/_inline_${env}`) %> + + <% } %> <% } else if (dynatraceFlag.treatment === 'global-cdn') { %> <% if (useNewRUM) { %> <%- include(`./partials/dynatrace/_inline_${env}_new`) %> + + <% } else { %> + + <% } %> <% } %> - - - <% } %> From bc00f0b1fd993efb05c47637ba102798d529ae3d Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Mon, 1 Jun 2026 04:50:21 -0600 Subject: [PATCH 10/43] Bump react-scripts to 8.17.0-alpha.4 Fixes telemetry timing to report RUM loading mechanism before first page summary beacon. Co-Authored-By: Claude Haiku 4.5 --- packages/react-scripts/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index 9ef50740ee8..73d87455850 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -1,6 +1,6 @@ { "name": "@fs/react-scripts", - "version": "8.17.0-alpha.3", + "version": "8.17.0-alpha.4", "upstreamVersion": "5.0.1", "description": "Configuration and scripts for Create React App.", "repository": { From 29ce2e3c19a89feee1037a9393badbfd37a2be5b Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Mon, 1 Jun 2026 05:16:04 -0600 Subject: [PATCH 11/43] Add async attribute to old RUM asyncCS-script mechanism asyncCS-script should load the script asynchronously to not block page rendering. Add missing async attribute to old RUM Edge CDN script. Co-Authored-By: Claude Haiku 4.5 --- packages/react-scripts/layout/views/partials/dynatrace.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-scripts/layout/views/partials/dynatrace.ejs b/packages/react-scripts/layout/views/partials/dynatrace.ejs index a23c33df052..354641751ed 100644 --- a/packages/react-scripts/layout/views/partials/dynatrace.ejs +++ b/packages/react-scripts/layout/views/partials/dynatrace.ejs @@ -53,7 +53,7 @@ if (typeof getFeatureFlag !== 'undefined') { } <% } else { %> - + <% } else { %> - + + <% } else { %> - + + + <% } else { %> + \ No newline at end of file + \ No newline at end of file diff --git a/packages/react-scripts/layout/views/partials/dynatrace/_inline_int_new.ejs b/packages/react-scripts/layout/views/partials/dynatrace/_inline_int_new.ejs index b8cfb504233..6308c6990b8 100644 --- a/packages/react-scripts/layout/views/partials/dynatrace/_inline_int_new.ejs +++ b/packages/react-scripts/layout/views/partials/dynatrace/_inline_int_new.ejs @@ -1 +1,946 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/react-scripts/layout/views/partials/dynatrace/_inline_prod_new.ejs b/packages/react-scripts/layout/views/partials/dynatrace/_inline_prod_new.ejs index d3e7c6e2d1f..5b054479c81 100644 --- a/packages/react-scripts/layout/views/partials/dynatrace/_inline_prod_new.ejs +++ b/packages/react-scripts/layout/views/partials/dynatrace/_inline_prod_new.ejs @@ -1 +1,609 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/react-scripts/scripts/fetch-dynatrace-scripts.js b/packages/react-scripts/scripts/fetch-dynatrace-scripts.js index da0d39dd819..fd0759e786e 100755 --- a/packages/react-scripts/scripts/fetch-dynatrace-scripts.js +++ b/packages/react-scripts/scripts/fetch-dynatrace-scripts.js @@ -186,30 +186,36 @@ async function fetchScripts() { console.log(`πŸ“₯ Fetching ${env.toUpperCase()} environment (entity: ${entityId})...`); try { - // Fetch OneAgent JavaScript tag with SRI (includes inline script and complete tag) - const path = `${basePath}/api/v2/rum/oneAgentJavaScriptTagWithSri/${entityId}`; - const response = await makeRequest(hostname, path); + // Fetch OneAgent JavaScript tag with SRI (for CDN URL, config, integrity hash) + const tagPath = `${basePath}/api/v2/rum/oneAgentJavaScriptTagWithSri/${entityId}`; + const tagResponse = await makeRequest(hostname, tagPath); + + // Fetch inline code (actual JavaScript to embed) + const inlinePath = `${basePath}/api/v2/rum/inlineCode/${entityId}`; + const inlineResponse = await makeRequest(hostname, inlinePath); results[env] = { - completeTag: response.trim(), + completeTag: tagResponse.trim(), + inlineCode: inlineResponse.trim(), }; - console.log(` βœ… Complete tag fetched (${response.length} chars)`); + console.log(` βœ… Complete tag fetched (${tagResponse.length} chars)`); + console.log(` βœ… Inline code fetched (${inlineResponse.length} chars)`); // Extract CDN URL from complete tag - const srcMatch = response.match(/src="([^"]+)"/); + const srcMatch = tagResponse.match(/src="([^"]+)"/); if (srcMatch) { console.log(` πŸ“Œ CDN URL: ${srcMatch[1]}`); } // Extract integrity hash if present - const integrityMatch = response.match(/integrity="([^"]+)"/); + const integrityMatch = tagResponse.match(/integrity="([^"]+)"/); if (integrityMatch) { console.log(` πŸ”’ Integrity: ${integrityMatch[1]}`); } // Extract data-dtconfig if present - const configMatch = response.match(/data-dtconfig="([^"]+)"/); + const configMatch = tagResponse.match(/data-dtconfig="([^"]+)"/); if (configMatch) { console.log(` βš™οΈ Config includes reportUrl for direct beacon delivery`); } @@ -230,7 +236,7 @@ async function fetchScripts() { const inlineFilePath = path.join(inlineScriptDir, `_inline_${env}_new.ejs`); try { - fs.writeFileSync(inlineFilePath, data.completeTag, 'utf8'); + fs.writeFileSync(inlineFilePath, data.inlineCode, 'utf8'); console.log(` βœ… Wrote ${path.basename(inlineFilePath)}`); } catch (error) { console.error(` ❌ Failed to write ${env} inline script: ${error.message}`); From 341fb900e3870a35946462bd4d55eec1b3eb8e57 Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Mon, 1 Jun 2026 09:17:34 -0600 Subject: [PATCH 14/43] Call all three Dynatrace RUM API endpoints Fetch and display: - /api/v2/rum/javaScriptTag (simple tag, 157 chars) - /api/v2/rum/oneAgentJavaScriptTagWithSri (SRI tag, ~1KB) - /api/v2/rum/inlineCode (inline code, 300-460KB) Shows both CDN URLs for comparison: basic variant vs new SRI variant. Co-Authored-By: Claude Haiku 4.5 --- .../scripts/fetch-dynatrace-scripts.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/react-scripts/scripts/fetch-dynatrace-scripts.js b/packages/react-scripts/scripts/fetch-dynatrace-scripts.js index fd0759e786e..a0c868d321c 100755 --- a/packages/react-scripts/scripts/fetch-dynatrace-scripts.js +++ b/packages/react-scripts/scripts/fetch-dynatrace-scripts.js @@ -186,6 +186,10 @@ async function fetchScripts() { console.log(`πŸ“₯ Fetching ${env.toUpperCase()} environment (entity: ${entityId})...`); try { + // Fetch simple JavaScript tag (basic, no SRI) + const simplePath = `${basePath}/api/v2/rum/javaScriptTag/${entityId}`; + const simpleResponse = await makeRequest(hostname, simplePath); + // Fetch OneAgent JavaScript tag with SRI (for CDN URL, config, integrity hash) const tagPath = `${basePath}/api/v2/rum/oneAgentJavaScriptTagWithSri/${entityId}`; const tagResponse = await makeRequest(hostname, tagPath); @@ -195,17 +199,24 @@ async function fetchScripts() { const inlineResponse = await makeRequest(hostname, inlinePath); results[env] = { + simpleTag: simpleResponse.trim(), completeTag: tagResponse.trim(), inlineCode: inlineResponse.trim(), }; + console.log(` βœ… Simple tag fetched (${simpleResponse.length} chars)`); console.log(` βœ… Complete tag fetched (${tagResponse.length} chars)`); console.log(` βœ… Inline code fetched (${inlineResponse.length} chars)`); - // Extract CDN URL from complete tag - const srcMatch = tagResponse.match(/src="([^"]+)"/); - if (srcMatch) { - console.log(` πŸ“Œ CDN URL: ${srcMatch[1]}`); + // Extract CDN URL from both simple and complete tags + const simpleSrcMatch = simpleResponse.match(/src="([^"]+)"/); + if (simpleSrcMatch) { + console.log(` πŸ“Œ Simple CDN URL: ${simpleSrcMatch[1]}`); + } + + const sriBenchMatch = tagResponse.match(/src="([^"]+)"/); + if (sriBenchMatch) { + console.log(` πŸ“Œ SRI CDN URL: ${sriBenchMatch[1]}`); } // Extract integrity hash if present From 83ddfa9ad58026b71d263aee088e2c20ec423d5f Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Mon, 1 Jun 2026 14:52:16 -0600 Subject: [PATCH 15/43] Improve fetch script with refactored helpers and CDN config export - Extract env values into separate helper function for reusability - Update dynatrace.ejs with per-env fallback pattern using locals.dynatrace - Generate dynatrace-rum-config.json for Snow to publish to CDN - Snow can inject fresh values at runtime via locals.dynatrace override - Clearer next steps in console output This enables decoupling: react-scripts has built-in fallback values, but Snow can override with latest values from dynatrace-rum-config.json CDN file. Co-Authored-By: Claude Haiku 4.5 --- .../scripts/dynatrace-rum-config.json | 23 ++++++ .../scripts/fetch-dynatrace-scripts.js | 82 ++++++++++++------- 2 files changed, 75 insertions(+), 30 deletions(-) create mode 100644 packages/react-scripts/scripts/dynatrace-rum-config.json diff --git a/packages/react-scripts/scripts/dynatrace-rum-config.json b/packages/react-scripts/scripts/dynatrace-rum-config.json new file mode 100644 index 00000000000..c806e5eb863 --- /dev/null +++ b/packages/react-scripts/scripts/dynatrace-rum-config.json @@ -0,0 +1,23 @@ +{ + "generated": "2026-06-01T20:51:36.349Z", + "cdnUrlsNew": { + "int": "https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA15789NPQRTUVXfghqrux_10337260504112723.js", + "beta": "https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA7NQVfghqrux_10337260504112723.js", + "prod": "https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA7NQVfghqrux_10337260504112723.js" + }, + "cdnIntegrityNew": { + "int": "sha256-KHFYWCusB88n37My9yKVX/ES9BX6Wy+k74hsDScU5jY=", + "beta": "sha256-6PrnNTp1/05LJTqnB9OOhKEVMId1XHk4Xa1x3cPF2Ec=", + "prod": "sha256-6PrnNTp1/05LJTqnB9OOhKEVMId1XHk4Xa1x3cPF2Ec=" + }, + "cdnConfigNew": { + "int": "app=3faf90e849295814|cors=1|owasp=1|featureHash=ICA15789NPQRTUVXfghqrux|msl=153600|srsr=10000|nsfnv=1|reportUrl=https://bf99293tkn.bf.dynatrace.com/bf|srvr=%5C%2Fidentity%5C%2Fsettings|rdnt=2|uxrgce=1|cuc=t0rtn87t|srms=2,1,0,0%2Ftextarea%2Cinput%2Cselect%2Coption;0%2Fdatalist;0%2Fform%20button;0%2F%5Bdata-dtrum-input%5D;0%2F.data-dtrum-input;1%2F%5Edata%28%28%5C-.%2B%24%29%7C%24%29|mel=100000|dpvc=1|md=mdcc1=cfs_anid,mdcc2=bFS.User.profile.cisId,mdcc3=bs.pageName,mdcc4=bFS.attrs.anonId,mdcc5=bFS.attrs.country,mdcc6=bFS.attrs.cisId|lastModification=1780298395720|mdp=mdcc4,mdcc5|tp=500,50,0|srbbv=2|agentUri=https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA15789NPQRTUVXfghqrux_10337260504112723.js", + "beta": "app=c4242bb1eb216374|cors=1|owasp=1|featureHash=ICA7NQVfghqrux|msl=153600|srsr=10000|nsfnv=1|reportUrl=https://bf99293tkn.bf.dynatrace.com/bf|srvr=%5C%2Fidentity%5C%2Fsettings|rdnt=2|uxrgce=1|cuc=t0rtn87t|srms=2,1,0,0%2Ftextarea%2Cinput%2Cselect%2Coption;0%2Fdatalist;0%2Fform%20button;0%2F%5Bdata-dtrum-input%5D;0%2F.data-dtrum-input;1%2F%5Edata%28%28%5C-.%2B%24%29%7C%24%29|mel=100000|dpvc=1|md=mdcc1=cfs_anid,mdcc2=bFS.attrs.cisId,mdcc3=bFS.User.profile.cisId,mdcc4=fx-ratelimit-remaining,mdcc5=bs.campaign,mdcc6=bs.channel,mdcc7=bs.pageName,mdcc8=bs.pageURL,mdcc9=bs.pageType,mdcc10=bs.visitorID|lastModification=1780298395720|tp=500,50,0|srbbv=2|agentUri=https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA7NQVfghqrux_10337260504112723.js", + "prod": "app=a8e5edd77f861ace|cors=1|owasp=1|featureHash=ICA7NQVfghqrux|msl=153600|srsr=10000|vcx=1500|nsfnv=1|reportUrl=https://bf99293tkn.bf.dynatrace.com/bf|srvr=%5C%2Fidentity%5C%2Fsettings|rdnt=2|uxrgce=1|vcit=8000|cuc=t0rtn87t|srms=2,2,1,|mdl=mdcc11=20|mel=100000|dpvc=1|md=mdcc1=cfs_anid,mdcc2=bFS.attrs.cisId,mdcc3=bFS.User.profile.cisId,mdcc4=bs.pageName,mdcc5=bs.pageType,mdcc6=bs.pageURL,mdcc7=bs.visitorID,mdcc8=bs.campaign,mdcc9=bs.channel,mdcc10=fx-ratelimit-remaining,mdcc11=bdocument.referrer,mdcc12=dutm_campaign,mdcc13=dutm_source,mdcc14=dutm_medium|lastModification=1780298395720|tp=500,50,0|srbbv=2|agentUri=https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA7NQVfghqrux_10337260504112723.js" + }, + "inlineScriptNew": { + "int": "", + "beta": "", + "prod": "" + } +} \ No newline at end of file diff --git a/packages/react-scripts/scripts/fetch-dynatrace-scripts.js b/packages/react-scripts/scripts/fetch-dynatrace-scripts.js index a0c868d321c..cf971678a66 100755 --- a/packages/react-scripts/scripts/fetch-dynatrace-scripts.js +++ b/packages/react-scripts/scripts/fetch-dynatrace-scripts.js @@ -119,45 +119,63 @@ async function makeRequest(hostname, path) { }); } +function extractEnvValues(results) { + const envs = Object.keys(results); + return { + cdnUrlsNew: Object.fromEntries(envs.map(env => [ + env, results[env].completeTag.match(/src="([^"]+)"/)?.[1] || '' + ])), + cdnIntegrityNew: Object.fromEntries(envs.map(env => [ + env, results[env].completeTag.match(/integrity="([^"]+)"/)?.[1] || '' + ])), + cdnConfigNew: Object.fromEntries(envs.map(env => [ + env, results[env].completeTag.match(/data-dtconfig="([^"]+)"/)?.[1] || '' + ])), + inlineScriptNew: Object.fromEntries(envs.map(env => [ + env, results[env].completeTag.trim() + ])), + }; +} + function updateDynatraceEjs(results) { const ejsPath = path.join(__dirname, '../layout/views/partials/dynatrace.ejs'); let content = fs.readFileSync(ejsPath, 'utf8'); + const values = extractEnvValues(results); - // Build replacement cdnUrlsNew block - const urlLines = Object.entries(results).map(([env, data]) => { - const src = data.completeTag.match(/src="([^"]+)"/)?.[1] || ''; - return ` ${env}: '${src}'`; - }).join(',\n'); + const urlLines = Object.entries(values.cdnUrlsNew).map(([env, url]) => + ` ${env}: '${url}'`).join(',\n'); content = content.replace( - /const cdnUrlsNew = \{[^}]+\}/s, - `const cdnUrlsNew = {\n${urlLines}\n }` + /const cdnUrlsNew = locals\.dynatrace\?\.cdnUrlsNew \|\| \{[^}]+\}/s, + `const cdnUrlsNew = locals.dynatrace?.cdnUrlsNew || {\n${urlLines}\n }` ); - // Build replacement cdnIntegrityNew block (per-env object) - const integrityLines = Object.entries(results).map(([env, data]) => { - const hash = data.completeTag.match(/integrity="([^"]+)"/)?.[1] || ''; - return ` ${env}: '${hash}'`; - }).join(',\n'); + const integrityLines = Object.entries(values.cdnIntegrityNew).map(([env, hash]) => + ` ${env}: '${hash}'`).join(',\n'); content = content.replace( - /const cdnIntegrityNew = (\{[^}]+\}|'[^']+')/s, - `const cdnIntegrityNew = {\n${integrityLines}\n }` + /const cdnIntegrityNew = locals\.dynatrace\?\.cdnIntegrityNew \|\| \{[^}]+\}/s, + `const cdnIntegrityNew = locals.dynatrace?.cdnIntegrityNew || {\n${integrityLines}\n }` ); - // Build replacement cdnConfigNew block (per-env object with data-dtconfig) - const configLines = Object.entries(results).map(([env, data]) => { - const config = data.completeTag.match(/data-dtconfig="([^"]+)"/)?.[1] || ''; - return ` ${env}: '${config}'`; - }).join(',\n'); + const configLines = Object.entries(values.cdnConfigNew).map(([env, cfg]) => + ` ${env}: '${cfg}'`).join(',\n'); content = content.replace( - /const cdnConfigNew = (\{[^}]+\}|'[^']+')/s, - `const cdnConfigNew = {\n${configLines}\n }` - ) || content.replace( - /const cdnIntegrityNew = \{[^}]+\}/s, - match => `${match}\n\n const cdnConfigNew = {\n${configLines}\n }` + /const cdnConfigNew = locals\.dynatrace\?\.cdnConfigNew \|\| \{[^}]+\}/s, + `const cdnConfigNew = locals.dynatrace?.cdnConfigNew || {\n${configLines}\n }` ); fs.writeFileSync(ejsPath, content, 'utf8'); - console.log(" βœ… Updated dynatrace.ejs (cdnUrlsNew + cdnIntegrityNew + cdnConfigNew)"); + console.log(" βœ… Updated dynatrace.ejs fallback values"); +} + +function writeCdnConfigJson(results) { + const values = extractEnvValues(results); + const config = { + generated: new Date().toISOString(), + ...values, + }; + const configPath = path.join(__dirname, 'dynatrace-rum-config.json'); + fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8'); + console.log(" βœ… Wrote dynatrace-rum-config.json (publish this to CDN for Snow)"); } async function fetchScripts() { @@ -254,14 +272,18 @@ async function fetchScripts() { } } - // Update dynatrace.ejs with new CDN URLs and per-env integrity hashes + // Update dynatrace.ejs fallback values and write CDN config JSON + console.log("\nπŸ“ Updating files...\n"); updateDynatraceEjs(results); + writeCdnConfigJson(results); - console.log("\n✨ Scripts fetched and all files updated successfully!"); + console.log("\n✨ Done!"); console.log("\nNext steps:"); - console.log("1. βœ… Inline scripts written to _inline_*_new.ejs files"); - console.log("2. βœ… dynatrace.ejs updated with new CDN URLs and integrity hashes"); - console.log("3. Test all three mechanisms (asyncCS-script, asyncCS-inline, global-cdn)"); + console.log("1. βœ… _inline_*_new.ejs files written"); + console.log("2. βœ… dynatrace.ejs fallback values updated"); + console.log("3. βœ… dynatrace-rum-config.json written β€” publish to CDN for Snow to consume"); + console.log("4. Commit and deploy react-scripts to update the fallback values"); + console.log("5. Test all three mechanisms (asyncCS-script, asyncCS-inline, global-cdn)"); return results; } catch (error) { From 6b3490742cc816f737fc4f272a1741b602aab86d Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Mon, 1 Jun 2026 14:53:49 -0600 Subject: [PATCH 16/43] Add locals.dynatrace fallback pattern for progressive enhancement Update dynatrace.ejs to support Snow providing fresh config values via res.locals.dynatrace, with fallback to hardcoded values. This enables: - Old Snow: transparently uses hardcoded fallback values - New Snow: overrides with fresh values from CDN config file - Old react-scripts + new Snow: new RUM works with stale but functional config - New react-scripts + old Snow: new RUM works with publish-time config For asyncCS-inline, also check for Snow-provided inline script before falling back to included file. This completes the progressive enhancement architecture where react-scripts carries current values at publish time, and Snow can override them dynamically without breaking older app/runtime combinations. Co-Authored-By: Claude Haiku 4.5 --- .../layout/views/partials/dynatrace.ejs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/react-scripts/layout/views/partials/dynatrace.ejs b/packages/react-scripts/layout/views/partials/dynatrace.ejs index 8b3c6ee6577..0358473e1e1 100644 --- a/packages/react-scripts/layout/views/partials/dynatrace.ejs +++ b/packages/react-scripts/layout/views/partials/dynatrace.ejs @@ -20,20 +20,21 @@ if (typeof getFeatureFlag !== 'undefined') { prod: 'https://edge.fscdn.org/assets/components/hf/assets/js/monitoring/dynatrace-20221104-prod.min-44bb9345beec8a984c2fca40385b4f41.js' } - // New RUM 1.329+ version URLs (SRI variant with integrity hashes) - const cdnUrlsNew = { + // New RUM 1.329+ β€” Snow provides fresh values via locals.dynatrace (from CDN config file); + // fallback to values current at react-scripts publish time. + const cdnUrlsNew = locals.dynatrace?.cdnUrlsNew || { int: 'https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA15789NPQRTUVXfghqrux_10337260504112723.js', beta: 'https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA7NQVfghqrux_10337260504112723.js', prod: 'https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA7NQVfghqrux_10337260504112723.js' } - const cdnIntegrityNew = { + const cdnIntegrityNew = locals.dynatrace?.cdnIntegrityNew || { int: 'sha256-KHFYWCusB88n37My9yKVX/ES9BX6Wy+k74hsDScU5jY=', beta: 'sha256-6PrnNTp1/05LJTqnB9OOhKEVMId1XHk4Xa1x3cPF2Ec=', prod: 'sha256-6PrnNTp1/05LJTqnB9OOhKEVMId1XHk4Xa1x3cPF2Ec=' } - const cdnConfigNew = { + const cdnConfigNew = locals.dynatrace?.cdnConfigNew || { int: 'app=3faf90e849295814|cors=1|owasp=1|featureHash=ICA15789NPQRTUVXfghqrux|msl=153600|srsr=10000|nsfnv=1|reportUrl=https://bf99293tkn.bf.dynatrace.com/bf|srvr=%5C%2Fidentity%5C%2Fsettings|rdnt=2|uxrgce=1|cuc=t0rtn87t|srms=2,1,0,0%2Ftextarea%2Cinput%2Cselect%2Coption;0%2Fdatalist;0%2Fform%20button;0%2F%5Bdata-dtrum-input%5D;0%2F.data-dtrum-input;1%2F%5Edata%28%28%5C-.%2B%24%29%7C%24%29|mel=100000|dpvc=1|md=mdcc1=cfs_anid,mdcc2=bFS.User.profile.cisId,mdcc3=bs.pageName,mdcc4=bFS.attrs.anonId,mdcc5=bFS.attrs.country,mdcc6=bFS.attrs.cisId|lastModification=1780298395720|mdp=mdcc4,mdcc5|tp=500,50,0|srbbv=2|agentUri=https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA15789NPQRTUVXfghqrux_10337260504112723.js', beta: 'app=c4242bb1eb216374|cors=1|owasp=1|featureHash=ICA7NQVfghqrux|msl=153600|srsr=10000|nsfnv=1|reportUrl=https://bf99293tkn.bf.dynatrace.com/bf|srvr=%5C%2Fidentity%5C%2Fsettings|rdnt=2|uxrgce=1|cuc=t0rtn87t|srms=2,1,0,0%2Ftextarea%2Cinput%2Cselect%2Coption;0%2Fdatalist;0%2Fform%20button;0%2F%5Bdata-dtrum-input%5D;0%2F.data-dtrum-input;1%2F%5Edata%28%28%5C-.%2B%24%29%7C%24%29|mel=100000|dpvc=1|md=mdcc1=cfs_anid,mdcc2=bFS.attrs.cisId,mdcc3=bFS.User.profile.cisId,mdcc4=fx-ratelimit-remaining,mdcc5=bs.campaign,mdcc6=bs.channel,mdcc7=bs.pageName,mdcc8=bs.pageURL,mdcc9=bs.pageType,mdcc10=bs.visitorID|lastModification=1780298395720|tp=500,50,0|srbbv=2|agentUri=https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA7NQVfghqrux_10337260504112723.js', prod: 'app=a8e5edd77f861ace|cors=1|owasp=1|featureHash=ICA7NQVfghqrux|msl=153600|srsr=10000|vcx=1500|nsfnv=1|reportUrl=https://bf99293tkn.bf.dynatrace.com/bf|srvr=%5C%2Fidentity%5C%2Fsettings|rdnt=2|uxrgce=1|vcit=8000|cuc=t0rtn87t|srms=2,2,1,|mdl=mdcc11=20|mel=100000|dpvc=1|md=mdcc1=cfs_anid,mdcc2=bFS.attrs.cisId,mdcc3=bFS.User.profile.cisId,mdcc4=bs.pageName,mdcc5=bs.pageType,mdcc6=bs.pageURL,mdcc7=bs.visitorID,mdcc8=bs.campaign,mdcc9=bs.channel,mdcc10=fx-ratelimit-remaining,mdcc11=bdocument.referrer,mdcc12=dutm_campaign,mdcc13=dutm_source,mdcc14=dutm_medium|lastModification=1780298395720|tp=500,50,0|srbbv=2|agentUri=https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA7NQVfghqrux_10337260504112723.js' @@ -64,7 +65,11 @@ if (typeof getFeatureFlag !== 'undefined') { <% } %> <% } else if (dynatraceFlag.treatment === 'asyncCS-inline') { %> <% if (useNewRUM) { %> - <%- include(`./partials/dynatrace/_inline_${env}_new`) %> + <% if (locals.dynatrace?.inlineScriptNew?.[env]) { %> + <%- locals.dynatrace.inlineScriptNew[env] %> + <% } else { %> + <%- include(`./partials/dynatrace/_inline_${env}_new`) %> + <% } %> ", - "beta": "", - "prod": "" + "inlineScript": { + "int": "(minified script code here)", + "beta": "(minified script code here)", + "prod": "(minified script code here)" } -} \ No newline at end of file +} diff --git a/packages/react-scripts/scripts/fetch-dynatrace-scripts.js b/packages/react-scripts/scripts/fetch-dynatrace-scripts.js index 992380b80b4..50992951e72 100755 --- a/packages/react-scripts/scripts/fetch-dynatrace-scripts.js +++ b/packages/react-scripts/scripts/fetch-dynatrace-scripts.js @@ -145,16 +145,16 @@ async function makeRequest(hostname, path) { function extractEnvValues(results) { const envs = Object.keys(results); return { - cdnUrlsNew: Object.fromEntries(envs.map(env => [ + cdnUrls: Object.fromEntries(envs.map(env => [ env, results[env].completeTag.match(/src="([^"]+)"/)?.[1] || '' ])), - cdnIntegrityNew: Object.fromEntries(envs.map(env => [ + cdnIntegrity: Object.fromEntries(envs.map(env => [ env, results[env].completeTag.match(/integrity="([^"]+)"/)?.[1] || '' ])), - cdnConfigNew: Object.fromEntries(envs.map(env => [ + cdnConfig: Object.fromEntries(envs.map(env => [ env, results[env].completeTag.match(/data-dtconfig="([^"]+)"/)?.[1] || '' ])), - inlineScriptNew: Object.fromEntries(envs.map(env => [ + inlineScript: Object.fromEntries(envs.map(env => [ env, results[env].completeTag.trim() ])), }; @@ -165,25 +165,25 @@ function updateDynatraceEjs(results) { let content = fs.readFileSync(ejsPath, 'utf8'); const values = extractEnvValues(results); - const urlLines = Object.entries(values.cdnUrlsNew).map(([env, url]) => + const urlLines = Object.entries(values.cdnUrls).map(([env, url]) => ` ${env}: '${url}'`).join(',\n'); content = content.replace( /const cdnUrlsNew = locals\.dynatrace\?\.cdnUrlsNew \|\| \{[^}]+\}/s, - `const cdnUrlsNew = locals.dynatrace?.cdnUrlsNew || {\n${urlLines}\n }` + `const cdnUrlsNew = locals.dynatrace?.cdnUrls || {\n${urlLines}\n }` ); - const integrityLines = Object.entries(values.cdnIntegrityNew).map(([env, hash]) => + const integrityLines = Object.entries(values.cdnIntegrity).map(([env, hash]) => ` ${env}: '${hash}'`).join(',\n'); content = content.replace( /const cdnIntegrityNew = locals\.dynatrace\?\.cdnIntegrityNew \|\| \{[^}]+\}/s, - `const cdnIntegrityNew = locals.dynatrace?.cdnIntegrityNew || {\n${integrityLines}\n }` + `const cdnIntegrityNew = locals.dynatrace?.cdnIntegrity || {\n${integrityLines}\n }` ); - const configLines = Object.entries(values.cdnConfigNew).map(([env, cfg]) => + const configLines = Object.entries(values.cdnConfig).map(([env, cfg]) => ` ${env}: '${cfg}'`).join(',\n'); content = content.replace( /const cdnConfigNew = locals\.dynatrace\?\.cdnConfigNew \|\| \{[^}]+\}/s, - `const cdnConfigNew = locals.dynatrace?.cdnConfigNew || {\n${configLines}\n }` + `const cdnConfigNew = locals.dynatrace?.cdnConfig || {\n${configLines}\n }` ); fs.writeFileSync(ejsPath, content, 'utf8'); From 0efa8f8b6ca7ce616d19141c9678b0c6740b2ae2 Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Mon, 1 Jun 2026 20:07:49 -0600 Subject: [PATCH 19/43] Make S3 config file publicly readable with --acl public-read Snow servers and other services can now fetch dynatrace-rum-config.json without requiring AWS credentials or special access. Co-Authored-By: Claude Haiku 4.5 --- packages/react-scripts/scripts/fetch-dynatrace-scripts.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-scripts/scripts/fetch-dynatrace-scripts.js b/packages/react-scripts/scripts/fetch-dynatrace-scripts.js index 50992951e72..143300e31b2 100755 --- a/packages/react-scripts/scripts/fetch-dynatrace-scripts.js +++ b/packages/react-scripts/scripts/fetch-dynatrace-scripts.js @@ -97,7 +97,7 @@ const DYNATRACE_ENVIRONMENT_URL = process.env.DYNATRACE_ENV_URL || "https://bjm3 const DYNATRACE_API_TOKEN = process.env.DYNATRACE_API_TOKEN || getKeychainSecret(KEYCHAIN_SERVICE_DYNATRACE); // S3 CDN configuration (for publishing dynatrace-rum-config.json via AWS CLI) -const S3_PUBLISH_BUCKET = process.env.S3_PUBLISH_BUCKET || "frontier-rum-config"; +const S3_PUBLISH_BUCKET = process.env.S3_PUBLISH_BUCKET || "fs-static-prod/assets/dynatrace"; const S3_PUBLISH_REGION = process.env.S3_PUBLISH_REGION || "us-east-1"; // Entity IDs for your RUM applications in each environment @@ -209,7 +209,7 @@ async function publishToS3(config) { fs.writeFileSync(tempFile, configJson, 'utf8'); // Use AWS CLI for S3 upload (leverages configured AWS credentials) - const awsCmd = `aws s3 cp "${tempFile}" s3://${S3_PUBLISH_BUCKET}/dynatrace-rum-config.json --region ${S3_PUBLISH_REGION} --metadata "generated=$(date +%s)" --cache-control "max-age=300"`; + const awsCmd = `aws s3 cp "${tempFile}" s3://${S3_PUBLISH_BUCKET}/dynatrace-rum-config.json --region ${S3_PUBLISH_REGION} --metadata "generated=$(date +%s)" --cache-control "max-age=300" --acl public-read`; execSync(awsCmd, { stdio: 'inherit' }); fs.unlinkSync(tempFile); From c292c3d89fa0419456df1fd96ba65d801b7b4e03 Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Tue, 9 Jun 2026 16:24:24 -0600 Subject: [PATCH 20/43] Fix EJS parsing error by using string concatenation in include statements Convert template literals to string concatenation in include() calls for asyncCS-inline, asyncCS-script, and global-cdn fallback branches. EJS parser cannot handle backticks inside template tags. Also consolidate reporting mechanism calls to reduce duplication. --- .../layout/views/partials/dynatrace.ejs | 67 +++++++------------ 1 file changed, 25 insertions(+), 42 deletions(-) diff --git a/packages/react-scripts/layout/views/partials/dynatrace.ejs b/packages/react-scripts/layout/views/partials/dynatrace.ejs index 36aa7968696..0b85b1e3a3d 100644 --- a/packages/react-scripts/layout/views/partials/dynatrace.ejs +++ b/packages/react-scripts/layout/views/partials/dynatrace.ejs @@ -45,66 +45,49 @@ if (typeof getFeatureFlag !== 'undefined') { if (dynatraceFlag.treatment === 'asyncCS-script') { %> <% if (useNewRUM) { %> - - - - + <% if (locals.dynatrace?.inlineScript?.[env]) { %> + <%- locals.dynatrace.inlineScript[env] %> + <% } else { %> + + <% include('./partials/dynatrace/_inline_' + env + '_new') %> + <% } %> <% } else { %> - - <% } %> + + <% } else if (dynatraceFlag.treatment === 'asyncCS-inline') { %> <% if (useNewRUM) { %> <% if (locals.dynatrace?.inlineScript?.[env]) { %> <%- locals.dynatrace.inlineScript[env] %> <% } else { %> - <%- include(`./partials/dynatrace/_inline_${env}_new`) %> + <% include('./partials/dynatrace/_inline_' + env + '_new') %> <% } %> - <% } else { %> - <%- include(`./partials/dynatrace/_inline_${env}`) %> - - + <% include('./partials/dynatrace/_inline_' + env) %> <% } %> <% } else if (dynatraceFlag.treatment === 'global-cdn') { %> <% if (useNewRUM) { %> - - - - + <% if (locals.dynatrace?.inlineScript?.[env]) { %> + <%- locals.dynatrace.inlineScript[env] %> + <% } else { %> + + <% include('./partials/dynatrace/_inline_' + env + '_new') %> + <% } %> <% } else { %> - - <% } %> <% } %> - + <% } %> From 1f4316398e02e22f465f944dbe2641ba32b08dc9 Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Wed, 10 Jun 2026 05:20:10 -0600 Subject: [PATCH 21/43] Add hyphen to include() tags for proper EJS output EJS requires <%- %> (with hyphen) to output included file content. Control-flow syntax <% %> cannot execute include() function calls. --- .../react-scripts/layout/views/partials/dynatrace.ejs | 8 ++++---- .../react-scripts/scripts/dynatrace-rum-config.json | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/react-scripts/layout/views/partials/dynatrace.ejs b/packages/react-scripts/layout/views/partials/dynatrace.ejs index 0b85b1e3a3d..6d1630b689d 100644 --- a/packages/react-scripts/layout/views/partials/dynatrace.ejs +++ b/packages/react-scripts/layout/views/partials/dynatrace.ejs @@ -49,7 +49,7 @@ if (typeof getFeatureFlag !== 'undefined') { <%- locals.dynatrace.inlineScript[env] %> <% } else { %> - <% include('./partials/dynatrace/_inline_' + env + '_new') %> + <%- include('./partials/dynatrace/_inline_' + env + '_new') %> <% } %> <% } else { %> @@ -66,11 +66,11 @@ if (typeof getFeatureFlag !== 'undefined') { <% if (locals.dynatrace?.inlineScript?.[env]) { %> <%- locals.dynatrace.inlineScript[env] %> <% } else { %> - <% include('./partials/dynatrace/_inline_' + env + '_new') %> + <%- include('./partials/dynatrace/_inline_' + env + '_new') %> <% } %> <% } else { %> - <% include('./partials/dynatrace/_inline_' + env) %> + <%- include('./partials/dynatrace/_inline_' + env) %> <% } %> <% } else if (dynatraceFlag.treatment === 'global-cdn') { %> <% if (useNewRUM) { %> @@ -78,7 +78,7 @@ if (typeof getFeatureFlag !== 'undefined') { <%- locals.dynatrace.inlineScript[env] %> <% } else { %> - <% include('./partials/dynatrace/_inline_' + env + '_new') %> + <%- include('./partials/dynatrace/_inline_' + env + '_new') %> <% } %> <% } else { %> diff --git a/packages/react-scripts/scripts/dynatrace-rum-config.json b/packages/react-scripts/scripts/dynatrace-rum-config.json index ab625373861..d3e8ce83364 100644 --- a/packages/react-scripts/scripts/dynatrace-rum-config.json +++ b/packages/react-scripts/scripts/dynatrace-rum-config.json @@ -1,5 +1,5 @@ { - "generated": "2026-06-01T20:51:36.349Z", + "generated": "2026-06-03T01:12:50.000Z", "cdnUrls": { "int": "https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA15789NPQRTUVXfghqrux_10337260504112723.js", "beta": "https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA7NQVfghqrux_10337260504112723.js", @@ -16,8 +16,8 @@ "prod": "app=a8e5edd77f861ace|cors=1|owasp=1|featureHash=ICA7NQVfghqrux|msl=153600|srsr=10000|vcx=1500|nsfnv=1|reportUrl=https://bf99293tkn.bf.dynatrace.com/bf|srvr=%5C%2Fidentity%5C%2Fsettings|rdnt=2|uxrgce=1|vcit=8000|cuc=t0rtn87t|srms=2,2,1,|mdl=mdcc11=20|mel=100000|dpvc=1|md=mdcc1=cfs_anid,mdcc2=bFS.attrs.cisId,mdcc3=bFS.User.profile.cisId,mdcc4=bs.pageName,mdcc5=bs.pageType,mdcc6=bs.pageURL,mdcc7=bs.visitorID,mdcc8=bs.campaign,mdcc9=bs.channel,mdcc10=fx-ratelimit-remaining,mdcc11=bdocument.referrer,mdcc12=dutm_campaign,mdcc13=dutm_source,mdcc14=dutm_medium|lastModification=1780298395720|tp=500,50,0|srbbv=2|agentUri=https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA7NQVfghqrux_10337260504112723.js" }, "inlineScript": { - "int": "(minified script code here)", - "beta": "(minified script code here)", - "prod": "(minified script code here)" + "int": "", + "beta": "", + "prod": "" } -} +} \ No newline at end of file From 49d8e8e06714868e3547658a10e98c19fd815834 Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Wed, 10 Jun 2026 05:20:40 -0600 Subject: [PATCH 22/43] Use standard EJS include syntax without hyphen Revert to <% include(...) %> syntax without hyphen, matching the pattern in layout.ejs. Remove leading './' from path since include() resolves relative to views directory. --- .../react-scripts/layout/views/partials/dynatrace.ejs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-scripts/layout/views/partials/dynatrace.ejs b/packages/react-scripts/layout/views/partials/dynatrace.ejs index 6d1630b689d..90d948c4387 100644 --- a/packages/react-scripts/layout/views/partials/dynatrace.ejs +++ b/packages/react-scripts/layout/views/partials/dynatrace.ejs @@ -49,7 +49,7 @@ if (typeof getFeatureFlag !== 'undefined') { <%- locals.dynatrace.inlineScript[env] %> <% } else { %> - <%- include('./partials/dynatrace/_inline_' + env + '_new') %> + <% include('partials/dynatrace/_inline_' + env + '_new') %> <% } %> <% } else { %> @@ -66,11 +66,11 @@ if (typeof getFeatureFlag !== 'undefined') { <% if (locals.dynatrace?.inlineScript?.[env]) { %> <%- locals.dynatrace.inlineScript[env] %> <% } else { %> - <%- include('./partials/dynatrace/_inline_' + env + '_new') %> + <% include('partials/dynatrace/_inline_' + env + '_new') %> <% } %> <% } else { %> - <%- include('./partials/dynatrace/_inline_' + env) %> + <% include('partials/dynatrace/_inline_' + env) %> <% } %> <% } else if (dynatraceFlag.treatment === 'global-cdn') { %> <% if (useNewRUM) { %> @@ -78,7 +78,7 @@ if (typeof getFeatureFlag !== 'undefined') { <%- locals.dynatrace.inlineScript[env] %> <% } else { %> - <%- include('./partials/dynatrace/_inline_' + env + '_new') %> + <% include('partials/dynatrace/_inline_' + env + '_new') %> <% } %> <% } else { %> From 89cd853ef2885519ac41e8fc67b6e9571be48ffd Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Wed, 10 Jun 2026 05:21:26 -0600 Subject: [PATCH 23/43] Compute dynamic paths before include to support variable resolution EJS include directive requires a static identifier or variable, not inline function calls. Pre-compute dynamic paths using string concatenation, then pass the variable to include for proper resolution. --- .../layout/views/partials/dynatrace.ejs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/react-scripts/layout/views/partials/dynatrace.ejs b/packages/react-scripts/layout/views/partials/dynatrace.ejs index 90d948c4387..4f83200862b 100644 --- a/packages/react-scripts/layout/views/partials/dynatrace.ejs +++ b/packages/react-scripts/layout/views/partials/dynatrace.ejs @@ -49,7 +49,8 @@ if (typeof getFeatureFlag !== 'undefined') { <%- locals.dynatrace.inlineScript[env] %> <% } else { %> - <% include('partials/dynatrace/_inline_' + env + '_new') %> + <% const inlineNewPath = 'partials/dynatrace/_inline_' + env + '_new'; %> + <% include inlineNewPath %> <% } %> <% } else { %> @@ -66,11 +67,13 @@ if (typeof getFeatureFlag !== 'undefined') { <% if (locals.dynatrace?.inlineScript?.[env]) { %> <%- locals.dynatrace.inlineScript[env] %> <% } else { %> - <% include('partials/dynatrace/_inline_' + env + '_new') %> + <% const inlineNewPath2 = 'partials/dynatrace/_inline_' + env + '_new'; %> + <% include inlineNewPath2 %> <% } %> <% } else { %> - <% include('partials/dynatrace/_inline_' + env) %> + <% const inlineOldPath = 'partials/dynatrace/_inline_' + env; %> + <% include inlineOldPath %> <% } %> <% } else if (dynatraceFlag.treatment === 'global-cdn') { %> <% if (useNewRUM) { %> @@ -78,7 +81,8 @@ if (typeof getFeatureFlag !== 'undefined') { <%- locals.dynatrace.inlineScript[env] %> <% } else { %> - <% include('partials/dynatrace/_inline_' + env + '_new') %> + <% const inlineNewPath3 = 'partials/dynatrace/_inline_' + env + '_new'; %> + <% include inlineNewPath3 %> <% } %> <% } else { %> From ee7c0dfb65e3cf6af02f221d8e4acdbbe5990fd8 Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Wed, 10 Jun 2026 05:22:41 -0600 Subject: [PATCH 24/43] Fix EJS parsing error by using string concatenation in include statements Convert template literals to string concatenation in include() calls for asyncCS-inline, asyncCS-script, and global-cdn fallback branches. EJS parser cannot handle backticks inside template tags. Use <%- include() %> with string concatenation to support dynamic path calculation. --- .../layout/views/partials/dynatrace.ejs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/react-scripts/layout/views/partials/dynatrace.ejs b/packages/react-scripts/layout/views/partials/dynatrace.ejs index 4f83200862b..6d1630b689d 100644 --- a/packages/react-scripts/layout/views/partials/dynatrace.ejs +++ b/packages/react-scripts/layout/views/partials/dynatrace.ejs @@ -49,8 +49,7 @@ if (typeof getFeatureFlag !== 'undefined') { <%- locals.dynatrace.inlineScript[env] %> <% } else { %> - <% const inlineNewPath = 'partials/dynatrace/_inline_' + env + '_new'; %> - <% include inlineNewPath %> + <%- include('./partials/dynatrace/_inline_' + env + '_new') %> <% } %> <% } else { %> @@ -67,13 +66,11 @@ if (typeof getFeatureFlag !== 'undefined') { <% if (locals.dynatrace?.inlineScript?.[env]) { %> <%- locals.dynatrace.inlineScript[env] %> <% } else { %> - <% const inlineNewPath2 = 'partials/dynatrace/_inline_' + env + '_new'; %> - <% include inlineNewPath2 %> + <%- include('./partials/dynatrace/_inline_' + env + '_new') %> <% } %> <% } else { %> - <% const inlineOldPath = 'partials/dynatrace/_inline_' + env; %> - <% include inlineOldPath %> + <%- include('./partials/dynatrace/_inline_' + env) %> <% } %> <% } else if (dynatraceFlag.treatment === 'global-cdn') { %> <% if (useNewRUM) { %> @@ -81,8 +78,7 @@ if (typeof getFeatureFlag !== 'undefined') { <%- locals.dynatrace.inlineScript[env] %> <% } else { %> - <% const inlineNewPath3 = 'partials/dynatrace/_inline_' + env + '_new'; %> - <% include inlineNewPath3 %> + <%- include('./partials/dynatrace/_inline_' + env + '_new') %> <% } %> <% } else { %> From 217b706e4fcf16bedd6396ca7a7e3eb72c182770 Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Wed, 10 Jun 2026 05:33:05 -0600 Subject: [PATCH 25/43] Add logging to dynatrace.ejs to debug code path execution Log which treatment and code path is taken during dynatrace initialization to confirm we're running the latest deployed version. --- .../react-scripts/layout/views/partials/dynatrace.ejs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/react-scripts/layout/views/partials/dynatrace.ejs b/packages/react-scripts/layout/views/partials/dynatrace.ejs index 6d1630b689d..211a0c6822e 100644 --- a/packages/react-scripts/layout/views/partials/dynatrace.ejs +++ b/packages/react-scripts/layout/views/partials/dynatrace.ejs @@ -6,6 +6,7 @@ if (typeof getFeatureFlag !== 'undefined') { const dynatraceNewFlag = getFeatureFlag('frontier_snow_dynatraceNewRUM', {appName: process.env.APP_NAME}); const env = envType(); const useNewRUM = dynatraceNewFlag.isOn; + console.log('[dynatrace.ejs] Loaded - env:', env, 'useNewRUM:', useNewRUM, 'treatment:', dynatraceFlag.treatment); // Old RUM version URLs const cdnUrls = { @@ -47,8 +48,10 @@ if (typeof getFeatureFlag !== 'undefined') { <% if (useNewRUM) { %> <% if (locals.dynatrace?.inlineScript?.[env]) { %> <%- locals.dynatrace.inlineScript[env] %> + <% console.log('[dynatrace.ejs] asyncCS-script: Using inlineScript from locals'); %> <% } else { %> + <% console.log('[dynatrace.ejs] asyncCS-script: Using fallback include for env=' + env); %> <%- include('./partials/dynatrace/_inline_' + env + '_new') %> <% } %> <% } else { %> @@ -65,19 +68,24 @@ if (typeof getFeatureFlag !== 'undefined') { <% if (useNewRUM) { %> <% if (locals.dynatrace?.inlineScript?.[env]) { %> <%- locals.dynatrace.inlineScript[env] %> + <% console.log('[dynatrace.ejs] asyncCS-inline: Using inlineScript from locals'); %> <% } else { %> + <% console.log('[dynatrace.ejs] asyncCS-inline: Using fallback include for env=' + env); %> <%- include('./partials/dynatrace/_inline_' + env + '_new') %> <% } %> <% } else { %> + <% console.log('[dynatrace.ejs] asyncCS-inline (old RUM): Using fallback include for env=' + env); %> <%- include('./partials/dynatrace/_inline_' + env) %> <% } %> <% } else if (dynatraceFlag.treatment === 'global-cdn') { %> <% if (useNewRUM) { %> <% if (locals.dynatrace?.inlineScript?.[env]) { %> <%- locals.dynatrace.inlineScript[env] %> + <% console.log('[dynatrace.ejs] global-cdn: Using inlineScript from locals'); %> <% } else { %> + <% console.log('[dynatrace.ejs] global-cdn: Using fallback include for env=' + env); %> <%- include('./partials/dynatrace/_inline_' + env + '_new') %> <% } %> <% } else { %> From 7161f80f4c6f9ee1962e2960ee0d3ad0f7adff13 Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Wed, 10 Jun 2026 06:12:47 -0600 Subject: [PATCH 26/43] Remove optional chaining syntax unsupported by EJS parser Replace optional chaining (?.) with standard property access checks. Pre-compute include paths as variables to avoid string concatenation in include() calls, which may confuse EJS parser. --- .../layout/views/partials/dynatrace.ejs | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/react-scripts/layout/views/partials/dynatrace.ejs b/packages/react-scripts/layout/views/partials/dynatrace.ejs index 211a0c6822e..a2681eb4b2e 100644 --- a/packages/react-scripts/layout/views/partials/dynatrace.ejs +++ b/packages/react-scripts/layout/views/partials/dynatrace.ejs @@ -23,19 +23,19 @@ if (typeof getFeatureFlag !== 'undefined') { // New RUM 1.329+ β€” Snow provides fresh values via locals.dynatrace (from CDN config file); // fallback to values current at react-scripts publish time. - const cdnUrlsNew = locals.dynatrace?.cdnUrls || { + const cdnUrlsNew = (locals && locals.dynatrace && locals.dynatrace.cdnUrls) || { int: 'https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA15789NPQRTUVXfghqrux_10337260504112723.js', beta: 'https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA7NQVfghqrux_10337260504112723.js', prod: 'https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA7NQVfghqrux_10337260504112723.js' } - const cdnIntegrityNew = locals.dynatrace?.cdnIntegrity || { + const cdnIntegrityNew = (locals && locals.dynatrace && locals.dynatrace.cdnIntegrity) || { int: 'sha256-KHFYWCusB88n37My9yKVX/ES9BX6Wy+k74hsDScU5jY=', beta: 'sha256-6PrnNTp1/05LJTqnB9OOhKEVMId1XHk4Xa1x3cPF2Ec=', prod: 'sha256-6PrnNTp1/05LJTqnB9OOhKEVMId1XHk4Xa1x3cPF2Ec=' } - const cdnConfigNew = locals.dynatrace?.cdnConfig || { + const cdnConfigNew = (locals && locals.dynatrace && locals.dynatrace.cdnConfig) || { int: 'app=3faf90e849295814|cors=1|owasp=1|featureHash=ICA15789NPQRTUVXfghqrux|msl=153600|srsr=10000|nsfnv=1|reportUrl=https://bf99293tkn.bf.dynatrace.com/bf|srvr=%5C%2Fidentity%5C%2Fsettings|rdnt=2|uxrgce=1|cuc=t0rtn87t|srms=2,1,0,0%2Ftextarea%2Cinput%2Cselect%2Coption;0%2Fdatalist;0%2Fform%20button;0%2F%5Bdata-dtrum-input%5D;0%2F.data-dtrum-input;1%2F%5Edata%28%28%5C-.%2B%24%29%7C%24%29|mel=100000|dpvc=1|md=mdcc1=cfs_anid,mdcc2=bFS.User.profile.cisId,mdcc3=bs.pageName,mdcc4=bFS.attrs.anonId,mdcc5=bFS.attrs.country,mdcc6=bFS.attrs.cisId|lastModification=1780298395720|mdp=mdcc4,mdcc5|tp=500,50,0|srbbv=2|agentUri=https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA15789NPQRTUVXfghqrux_10337260504112723.js', beta: 'app=c4242bb1eb216374|cors=1|owasp=1|featureHash=ICA7NQVfghqrux|msl=153600|srsr=10000|nsfnv=1|reportUrl=https://bf99293tkn.bf.dynatrace.com/bf|srvr=%5C%2Fidentity%5C%2Fsettings|rdnt=2|uxrgce=1|cuc=t0rtn87t|srms=2,1,0,0%2Ftextarea%2Cinput%2Cselect%2Coption;0%2Fdatalist;0%2Fform%20button;0%2F%5Bdata-dtrum-input%5D;0%2F.data-dtrum-input;1%2F%5Edata%28%28%5C-.%2B%24%29%7C%24%29|mel=100000|dpvc=1|md=mdcc1=cfs_anid,mdcc2=bFS.attrs.cisId,mdcc3=bFS.User.profile.cisId,mdcc4=fx-ratelimit-remaining,mdcc5=bs.campaign,mdcc6=bs.channel,mdcc7=bs.pageName,mdcc8=bs.pageURL,mdcc9=bs.pageType,mdcc10=bs.visitorID|lastModification=1780298395720|tp=500,50,0|srbbv=2|agentUri=https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA7NQVfghqrux_10337260504112723.js', prod: 'app=a8e5edd77f861ace|cors=1|owasp=1|featureHash=ICA7NQVfghqrux|msl=153600|srsr=10000|vcx=1500|nsfnv=1|reportUrl=https://bf99293tkn.bf.dynatrace.com/bf|srvr=%5C%2Fidentity%5C%2Fsettings|rdnt=2|uxrgce=1|vcit=8000|cuc=t0rtn87t|srms=2,2,1,|mdl=mdcc11=20|mel=100000|dpvc=1|md=mdcc1=cfs_anid,mdcc2=bFS.attrs.cisId,mdcc3=bFS.User.profile.cisId,mdcc4=bs.pageName,mdcc5=bs.pageType,mdcc6=bs.pageURL,mdcc7=bs.visitorID,mdcc8=bs.campaign,mdcc9=bs.channel,mdcc10=fx-ratelimit-remaining,mdcc11=bdocument.referrer,mdcc12=dutm_campaign,mdcc13=dutm_source,mdcc14=dutm_medium|lastModification=1780298395720|tp=500,50,0|srbbv=2|agentUri=https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA7NQVfghqrux_10337260504112723.js' @@ -44,15 +44,20 @@ if (typeof getFeatureFlag !== 'undefined') { const selectedCdnUrls = useNewRUM ? cdnUrlsNew : cdnUrls; const selectedCdnConfig = useNewRUM ? cdnConfigNew : {}; + // Pre-compute include paths to avoid EJS parsing issues with string concatenation + const inlinePathNew = './partials/dynatrace/_inline_' + env + '_new'; + const inlinePathOld = './partials/dynatrace/_inline_' + env; + const hasInlineScript = locals && locals.dynatrace && locals.dynatrace.inlineScript && locals.dynatrace.inlineScript[env]; + if (dynatraceFlag.treatment === 'asyncCS-script') { %> <% if (useNewRUM) { %> - <% if (locals.dynatrace?.inlineScript?.[env]) { %> + <% if (hasInlineScript) { %> <%- locals.dynatrace.inlineScript[env] %> <% console.log('[dynatrace.ejs] asyncCS-script: Using inlineScript from locals'); %> <% } else { %> <% console.log('[dynatrace.ejs] asyncCS-script: Using fallback include for env=' + env); %> - <%- include('./partials/dynatrace/_inline_' + env + '_new') %> + <%- include(inlinePathNew) %> <% } %> <% } else { %> @@ -66,27 +71,27 @@ if (typeof getFeatureFlag !== 'undefined') { <% } else if (dynatraceFlag.treatment === 'asyncCS-inline') { %> <% if (useNewRUM) { %> - <% if (locals.dynatrace?.inlineScript?.[env]) { %> + <% if (hasInlineScript) { %> <%- locals.dynatrace.inlineScript[env] %> <% console.log('[dynatrace.ejs] asyncCS-inline: Using inlineScript from locals'); %> <% } else { %> <% console.log('[dynatrace.ejs] asyncCS-inline: Using fallback include for env=' + env); %> - <%- include('./partials/dynatrace/_inline_' + env + '_new') %> + <%- include(inlinePathNew) %> <% } %> <% } else { %> <% console.log('[dynatrace.ejs] asyncCS-inline (old RUM): Using fallback include for env=' + env); %> - <%- include('./partials/dynatrace/_inline_' + env) %> + <%- include(inlinePathOld) %> <% } %> <% } else if (dynatraceFlag.treatment === 'global-cdn') { %> <% if (useNewRUM) { %> - <% if (locals.dynatrace?.inlineScript?.[env]) { %> + <% if (hasInlineScript) { %> <%- locals.dynatrace.inlineScript[env] %> <% console.log('[dynatrace.ejs] global-cdn: Using inlineScript from locals'); %> <% } else { %> <% console.log('[dynatrace.ejs] global-cdn: Using fallback include for env=' + env); %> - <%- include('./partials/dynatrace/_inline_' + env + '_new') %> + <%- include(inlinePathNew) %> <% } %> <% } else { %> From 538d7be5461e616e201940143d2aff37127d34e2 Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Mon, 13 Jul 2026 11:24:11 -0600 Subject: [PATCH 27/43] Load new Dynatrace RUM via script tags, never inline The new RUM agent's minified JS contains the "<%" sequence, which EJS include() misreads as an unterminated scriptlet ("Could not find matching close tag"), causing a 500. Load the new agent exclusively via <% } else { %> - + <% } %> - - <% } else if (dynatraceFlag.treatment === 'asyncCS-inline') { %> <% if (useNewRUM) { %> - <% if (hasInlineScript) { %> - <%- locals.dynatrace.inlineScript[env] %> - <% console.log('[dynatrace.ejs] asyncCS-inline: Using inlineScript from locals'); %> - <% } else { %> - <% console.log('[dynatrace.ejs] asyncCS-inline: Using fallback include for env=' + env); %> - <%- include(inlinePathNew) %> - <% } %> - + + <% } else { %> - <% console.log('[dynatrace.ejs] asyncCS-inline (old RUM): Using fallback include for env=' + env); %> + <%- include(inlinePathOld) %> <% } %> <% } else if (dynatraceFlag.treatment === 'global-cdn') { %> <% if (useNewRUM) { %> - <% if (hasInlineScript) { %> - <%- locals.dynatrace.inlineScript[env] %> - <% console.log('[dynatrace.ejs] global-cdn: Using inlineScript from locals'); %> - <% } else { %> - - <% console.log('[dynatrace.ejs] global-cdn: Using fallback include for env=' + env); %> - <%- include(inlinePathNew) %> - <% } %> + + <% } else { %> - - + + <% } %> <% } %> <% } %> diff --git a/packages/react-scripts/layout/views/partials/dynatrace/_inline_beta_new.ejs b/packages/react-scripts/layout/views/partials/dynatrace/_inline_beta_new.ejs deleted file mode 100644 index 2f05f6be375..00000000000 --- a/packages/react-scripts/layout/views/partials/dynatrace/_inline_beta_new.ejs +++ /dev/null @@ -1,609 +0,0 @@ - \ No newline at end of file diff --git a/packages/react-scripts/layout/views/partials/dynatrace/_inline_int_new.ejs b/packages/react-scripts/layout/views/partials/dynatrace/_inline_int_new.ejs deleted file mode 100644 index 6308c6990b8..00000000000 --- a/packages/react-scripts/layout/views/partials/dynatrace/_inline_int_new.ejs +++ /dev/null @@ -1,946 +0,0 @@ - \ No newline at end of file diff --git a/packages/react-scripts/layout/views/partials/dynatrace/_inline_prod_new.ejs b/packages/react-scripts/layout/views/partials/dynatrace/_inline_prod_new.ejs deleted file mode 100644 index 5b054479c81..00000000000 --- a/packages/react-scripts/layout/views/partials/dynatrace/_inline_prod_new.ejs +++ /dev/null @@ -1,609 +0,0 @@ - \ No newline at end of file diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index 73d87455850..d5dd33077c3 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -1,6 +1,6 @@ { "name": "@fs/react-scripts", - "version": "8.17.0-alpha.4", + "version": "8.17.0-alpha.12", "upstreamVersion": "5.0.1", "description": "Configuration and scripts for Create React App.", "repository": { diff --git a/packages/react-scripts/scripts/DYNATRACE_RUM_UPDATE.md b/packages/react-scripts/scripts/DYNATRACE_RUM_UPDATE.md index 442e573c4f8..cdcd74750be 100644 --- a/packages/react-scripts/scripts/DYNATRACE_RUM_UPDATE.md +++ b/packages/react-scripts/scripts/DYNATRACE_RUM_UPDATE.md @@ -43,9 +43,11 @@ node packages/react-scripts/scripts/fetch-dynatrace-scripts.js ``` The script will: -1. βœ… **Automatically write inline scripts** to `_inline_*_new.ejs` files -2. πŸ“Œ **Display CDN URLs** in copy-paste format -3. πŸ”’ **Display integrity hashes** for SRI verification +1. βœ… **Update the new-RUM fallback values** in `dynatrace.ejs` (`cdnUrlsNew`, `cdnIntegrityNew`, `cdnConfigNew`, `cdnCompleteUrlsNew`) +2. βœ… **Write & publish** `dynatrace-rum-config.json` to S3 so Snow can supply fresh values via `locals.dynatrace.*` without a react-scripts redeploy +3. πŸ”’ **Capture the SRI integrity hashes** and `data-dtconfig` for each environment + +> **The new RUM agent is never inlined.** The script no longer fetches `/inlineCode` or writes `_inline_*_new.ejs` files. Reason: EJS `include()` compiles the included file as a template, and the new agent's minified JS contains the two-character EJS open-delimiter sequence (a `<` immediately followed by a `%`) inside a string, which EJS misreads as an unterminated scriptlet ("Could not find matching close tag"). It also re-ships 300–460 KB on every page view. The new RUM loads exclusively via `", - "beta": "", - "prod": "" + "cdnCompleteUrls": { + "int": "https://js-cdn.dynatrace.com/jstag/15c157a40ab/bf99293tkn/3faf90e849295814_complete.js", + "beta": "https://js-cdn.dynatrace.com/jstag/15c157a40ab/bf99293tkn/c4242bb1eb216374_complete.js", + "prod": "https://js-cdn.dynatrace.com/jstag/15c157a40ab/bf99293tkn/a8e5edd77f861ace_complete.js" } } \ No newline at end of file diff --git a/packages/react-scripts/scripts/fetch-dynatrace-scripts.js b/packages/react-scripts/scripts/fetch-dynatrace-scripts.js index 143300e31b2..0750b5954f9 100755 --- a/packages/react-scripts/scripts/fetch-dynatrace-scripts.js +++ b/packages/react-scripts/scripts/fetch-dynatrace-scripts.js @@ -52,7 +52,7 @@ function getKeychainSecret(service) { { stdio: ['pipe', 'pipe', 'pipe'] } ).toString().trim(); return secret || null; - } catch { + } catch (error) { return null; } } @@ -97,7 +97,7 @@ const DYNATRACE_ENVIRONMENT_URL = process.env.DYNATRACE_ENV_URL || "https://bjm3 const DYNATRACE_API_TOKEN = process.env.DYNATRACE_API_TOKEN || getKeychainSecret(KEYCHAIN_SERVICE_DYNATRACE); // S3 CDN configuration (for publishing dynatrace-rum-config.json via AWS CLI) -const S3_PUBLISH_BUCKET = process.env.S3_PUBLISH_BUCKET || "fs-static-prod/assets/dynatrace"; +const S3_PUBLISH_BUCKET = process.env.S3_PUBLISH_BUCKET || "fs-cdn2-origin/assets/dynatrace"; const S3_PUBLISH_REGION = process.env.S3_PUBLISH_REGION || "us-east-1"; // Entity IDs for your RUM applications in each environment @@ -154,8 +154,9 @@ function extractEnvValues(results) { cdnConfig: Object.fromEntries(envs.map(env => [ env, results[env].completeTag.match(/data-dtconfig="([^"]+)"/)?.[1] || '' ])), - inlineScript: Object.fromEntries(envs.map(env => [ - env, results[env].completeTag.trim() + // Combined code+config file (the "JavaScript tag" _complete.js) used by the global-cdn treatment. + cdnCompleteUrls: Object.fromEntries(envs.map(env => [ + env, results[env].simpleTag.match(/src="([^"]+)"/)?.[1] || '' ])), }; } @@ -165,26 +166,28 @@ function updateDynatraceEjs(results) { let content = fs.readFileSync(ejsPath, 'utf8'); const values = extractEnvValues(results); - const urlLines = Object.entries(values.cdnUrls).map(([env, url]) => - ` ${env}: '${url}'`).join(',\n'); - content = content.replace( - /const cdnUrlsNew = locals\.dynatrace\?\.cdnUrlsNew \|\| \{[^}]+\}/s, - `const cdnUrlsNew = locals.dynatrace?.cdnUrls || {\n${urlLines}\n }` - ); - - const integrityLines = Object.entries(values.cdnIntegrity).map(([env, hash]) => - ` ${env}: '${hash}'`).join(',\n'); - content = content.replace( - /const cdnIntegrityNew = locals\.dynatrace\?\.cdnIntegrityNew \|\| \{[^}]+\}/s, - `const cdnIntegrityNew = locals.dynatrace?.cdnIntegrity || {\n${integrityLines}\n }` - ); - - const configLines = Object.entries(values.cdnConfig).map(([env, cfg]) => - ` ${env}: '${cfg}'`).join(',\n'); - content = content.replace( - /const cdnConfigNew = locals\.dynatrace\?\.cdnConfigNew \|\| \{[^}]+\}/s, - `const cdnConfigNew = locals.dynatrace?.cdnConfig || {\n${configLines}\n }` - ); + // Rewrite the hardcoded fallback object inside a + // const = (locals && locals.dynatrace && locals.dynatrace.) || { ... } + // block, preserving the locals-first progressive-enhancement guard. + const replaceFallback = (name, localsKey, map) => { + const lines = Object.entries(map).map(([env, v]) => ` ${env}: '${v}'`).join(',\n'); + const re = new RegExp( + `const ${name} = \\(locals && locals\\.dynatrace && locals\\.dynatrace\\.${localsKey}\\) \\|\\| \\{[^}]+\\}`, + 's' + ); + if (!re.test(content)) { + console.warn(` ⚠️ Could not find ${name} fallback block in dynatrace.ejs β€” skipped`); + return; + } + const replacement = `const ${name} = (locals && locals.dynatrace && locals.dynatrace.${localsKey}) || {\n${lines}\n }`; + // Use a function replacement so '$' in values is not treated as a substitution token. + content = content.replace(re, () => replacement); + }; + + replaceFallback('cdnUrlsNew', 'cdnUrls', values.cdnUrls); + replaceFallback('cdnIntegrityNew', 'cdnIntegrity', values.cdnIntegrity); + replaceFallback('cdnConfigNew', 'cdnConfig', values.cdnConfig); + replaceFallback('cdnCompleteUrlsNew', 'cdnCompleteUrls', values.cdnCompleteUrls); fs.writeFileSync(ejsPath, content, 'utf8'); console.log(" βœ… Updated dynatrace.ejs fallback values"); @@ -257,19 +260,16 @@ async function fetchScripts() { const tagPath = `${basePath}/api/v2/rum/oneAgentJavaScriptTagWithSri/${entityId}`; const tagResponse = await makeRequest(hostname, tagPath); - // Fetch inline code (actual JavaScript to embed) - const inlinePath = `${basePath}/api/v2/rum/inlineCode/${entityId}`; - const inlineResponse = await makeRequest(hostname, inlinePath); - + // NOTE: we intentionally no longer fetch /inlineCode. The new RUM agent is loaded via + // <% } %> diff --git a/packages/react-scripts/scripts/DYNATRACE_RUM_UPDATE.md b/packages/react-scripts/scripts/DYNATRACE_RUM_UPDATE.md index cdcd74750be..4885b1a3c3d 100644 --- a/packages/react-scripts/scripts/DYNATRACE_RUM_UPDATE.md +++ b/packages/react-scripts/scripts/DYNATRACE_RUM_UPDATE.md @@ -49,33 +49,19 @@ The script will: > **The new RUM agent is never inlined.** The script no longer fetches `/inlineCode` or writes `_inline_*_new.ejs` files. Reason: EJS `include()` compiles the included file as a template, and the new agent's minified JS contains the two-character EJS open-delimiter sequence (a `<` immediately followed by a `%`) inside a string, which EJS misreads as an unterminated scriptlet ("Could not find matching close tag"). It also re-ships 300–460 KB on every page view. The new RUM loads exclusively via ` diff --git a/packages/react-scripts/layout/views/partials/dynatrace.ejs b/packages/react-scripts/layout/views/partials/dynatrace.ejs index 3366eba5128..669a98011dd 100644 --- a/packages/react-scripts/layout/views/partials/dynatrace.ejs +++ b/packages/react-scripts/layout/views/partials/dynatrace.ejs @@ -2,42 +2,51 @@ /* SETS up dynatrace for the various environments */ if (typeof getFeatureFlag !== 'undefined') { + /* One flag, three states β€” see tools/dynatrace/DYNATRACE_RUM_MECHANISMS.md for the full rationale. + * + * 'off' no agent at all (kill switch) + * 'asyncCS-inline' versioned OneAgent JS tag + SRI, loaded SYNCHRONOUSLY + * anything else combined _complete.js from the Dynatrace global CDN, loaded ASYNC + * + * The default is deliberately the fallthrough rather than an explicit 'global-cdn' match: a + * retired treatment name, a renamed flag, or a Split outage returning 'control' must still load + * RUM. Failing closed would produce a silent monitoring gap, which is the one failure mode we + * cannot detect from the data itself. + * + * The SRI arm is the hedge for BOTH documented revisit triggers: it is versioned and immutably + * cached (so it survives Dynatrace never adding an ETag to _complete.js), and it is synchronous + * (so it has no blind window if async proves to lose early interactions or errors). Because its + * URL is cacheable for a year, the sync parse-block is paid only on the first uncached load. + * + * Whether the New RUM Experience is active is NOT a code concern β€” it is the tenant's + * `enabledOnGrail` setting, toggled per application in the Dynatrace UI with no deploy. + * The old frontier_snow_dynatraceNewRUM flag never controlled it; it only toggled `async`. + */ const dynatraceFlag = getFeatureFlag('frontier_snow_dynatraceRUM', {appName: process.env.APP_NAME}); - const dynatraceNewFlag = getFeatureFlag('frontier_snow_dynatraceNewRUM', {appName: process.env.APP_NAME}); const env = envType(); - const useNewRUM = dynatraceNewFlag.isOn; + const treatment = dynatraceFlag.treatment; + const rumDisabled = treatment === 'off'; + const useSriSync = treatment === 'asyncCS-inline'; - // Old RUM version URLs - const cdnUrls = { - int: 'https://js-cdn.dynatrace.com/jstag/15c157a40ab/bf99293tkn/3faf90e849295814_complete.js', - beta: 'https://js-cdn.dynatrace.com/jstag/15c157a40ab/bf99293tkn/c4242bb1eb216374_complete.js', - prod: 'https://js-cdn.dynatrace.com/jstag/15c157a40ab/bf99293tkn/a8e5edd77f861ace_complete.js' - } - - const edgeUrls = { - int: 'https://edge.fscdn.org/assets/components/hf/assets/js/monitoring/dynatrace-20221104-int.min-9aebd229f83ef7c845ae898f451bb617.js', - beta: 'https://edge.fscdn.org/assets/components/hf/assets/js/monitoring/dynatrace-20221104-beta.min-e859eab722487cc71d4f659447d7ed6d.js', - prod: 'https://edge.fscdn.org/assets/components/hf/assets/js/monitoring/dynatrace-20221104-prod.min-44bb9345beec8a984c2fca40385b4f41.js' - } - - // New RUM 1.329+ β€” Snow provides fresh values via locals.dynatrace (from CDN config file); + // Snow provides fresh values via locals.dynatrace (published to S3 by + // tools/dynatrace/fetch-dynatrace-scripts.js); fallback to values current at publish time. // fallback to values current at react-scripts publish time. const cdnUrlsNew = (locals && locals.dynatrace && locals.dynatrace.cdnUrls) || { - int: 'https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA15789NPQRTUVXfghqrux_10341260622154106.js', - beta: 'https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA7NQVfghqrux_10341260622154106.js', + int: 'https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA15789MNPQRTUVXfghqrux_10343260715123811.js', + beta: 'https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA15789NPRTUVXfghqrux_10343260715123811.js', prod: 'https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA7NQVfghqrux_10341260622154106.js' } const cdnIntegrityNew = (locals && locals.dynatrace && locals.dynatrace.cdnIntegrity) || { - int: 'sha256-xCOvz0J22cpI/MIdcbEaVWANEPMHmZAKpeD8zTuMY/s=', - beta: 'sha256-eGB6PLc8ndZISmjwbbi4xZhE9nbarKf48bRKxVdha+8=', + int: 'sha256-8SY/pAJr28AjGTRbzyKLcD4kPiJQIJaT2EfcY59FoWE=', + beta: 'sha256-PeyllVOccLjCaea3jYyf0w5YCMFa54GWyc4B3eRQn4g=', prod: 'sha256-eGB6PLc8ndZISmjwbbi4xZhE9nbarKf48bRKxVdha+8=' } const cdnConfigNew = (locals && locals.dynatrace && locals.dynatrace.cdnConfig) || { - int: 'app=3faf90e849295814|cors=1|owasp=1|featureHash=ICA15789NPQRTUVXfghqrux|msl=153600|srsr=10000|nsfnv=1|reportUrl=https://bf99293tkn.bf.dynatrace.com/bf|srvr=%5C%2Fidentity%5C%2Fsettings|rdnt=2|uxrgce=1|cuc=t0rtn87t|srms=2,1,0,0%2Ftextarea%2Cinput%2Cselect%2Coption;0%2Fdatalist;0%2Fform%20button;0%2F%5Bdata-dtrum-input%5D;0%2F.data-dtrum-input;1%2F%5Edata%28%28%5C-.%2B%24%29%7C%24%29|mel=100000|dpvc=1|md=mdcc1=cfs_anid,mdcc2=bFS.User.profile.cisId,mdcc3=bs.pageName,mdcc4=bFS.attrs.anonId,mdcc5=bFS.attrs.country,mdcc6=bFS.attrs.cisId|lastModification=1785320826721|mdp=mdcc4,mdcc5|tp=500,50,0|srbbv=2|agentUri=https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA15789NPQRTUVXfghqrux_10341260622154106.js', - beta: 'app=c4242bb1eb216374|cors=1|owasp=1|featureHash=ICA7NQVfghqrux|msl=153600|srsr=10000|nsfnv=1|reportUrl=https://bf99293tkn.bf.dynatrace.com/bf|srvr=%5C%2Fidentity%5C%2Fsettings|rdnt=2|uxrgce=1|cuc=t0rtn87t|srms=2,1,0,0%2Ftextarea%2Cinput%2Cselect%2Coption;0%2Fdatalist;0%2Fform%20button;0%2F%5Bdata-dtrum-input%5D;0%2F.data-dtrum-input;1%2F%5Edata%28%28%5C-.%2B%24%29%7C%24%29|mel=100000|dpvc=1|md=mdcc1=cfs_anid,mdcc2=bFS.attrs.cisId,mdcc3=bFS.User.profile.cisId,mdcc4=fx-ratelimit-remaining,mdcc5=bs.campaign,mdcc6=bs.channel,mdcc7=bs.pageName,mdcc8=bs.pageURL,mdcc9=bs.pageType,mdcc10=bs.visitorID|lastModification=1785320826721|tp=500,50,0|srbbv=2|agentUri=https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA7NQVfghqrux_10341260622154106.js', - prod: 'app=a8e5edd77f861ace|cors=1|owasp=1|featureHash=ICA7NQVfghqrux|msl=153600|srsr=10000|vcx=1500|nsfnv=1|reportUrl=https://bf99293tkn.bf.dynatrace.com/bf|srvr=%5C%2Fidentity%5C%2Fsettings|rdnt=2|uxrgce=1|vcit=8000|cuc=t0rtn87t|srms=2,2,1,|mdl=mdcc11=20|mel=100000|dpvc=1|md=mdcc1=cfs_anid,mdcc2=bFS.attrs.cisId,mdcc3=bFS.User.profile.cisId,mdcc4=bs.pageName,mdcc5=bs.pageType,mdcc6=bs.pageURL,mdcc7=bs.visitorID,mdcc8=bs.campaign,mdcc9=bs.channel,mdcc10=fx-ratelimit-remaining,mdcc11=bdocument.referrer,mdcc12=dutm_campaign,mdcc13=dutm_source,mdcc14=dutm_medium|lastModification=1785320826721|tp=500,50,0|srbbv=2|agentUri=https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA7NQVfghqrux_10341260622154106.js' + int: 'app=3faf90e849295814|cors=1|owasp=1|featureHash=ICA15789MNPQRTUVXfghqrux|msl=153600|srsr=10000|nsfnv=1|reportUrl=https://bf99293tkn.bf.dynatrace.com/bf|srvr=%5C%2Fidentity%5C%2Fsettings|rdnt=2|uxrgce=1|cuc=t0rtn87t|srms=2,1,0,0%2Ftextarea%2Cinput%2Cselect%2Coption;0%2Fdatalist;0%2Fform%20button;0%2F%5Bdata-dtrum-input%5D;0%2F.data-dtrum-input;0%2F%5Bcontenteditable%5D%3Anot%28%5Bcontenteditable%3D%22false%22%5D%29;0%2F%5Brole%3D%22textbox%22%5D%2C%5Brole%3D%22checkbox%22%5D%2C%5Brole%3D%22radio%22%5D%2C%5Brole%3D%22option%22%5D%2C%5Brole%3D%22combobox%22%5D%2C%5Brole%3D%22slider%22%5D%2C%5Brole%3D%22searchbox%22%5D%2C%5Brole%3D%22switch%22%5D%2C%5Brole%3D%22spinbutton%22%5D;1%2F%5Edata%28%28%5C-.%2B%24%29%7C%24%29|mel=100000|dpvc=1|md=mdcc1=cfs_anid,mdcc2=bFS.User.profile.cisId,mdcc3=bs.pageName,mdcc4=bFS.attrs.anonId,mdcc5=bFS.attrs.country,mdcc6=bFS.attrs.cisId|lastModification=1786000244457|mdp=mdcc4,mdcc5|tp=500,50,0|srbbv=2|agentUri=https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA15789MNPQRTUVXfghqrux_10343260715123811.js', + beta: 'app=c4242bb1eb216374|cors=1|owasp=1|featureHash=ICA15789NPRTUVXfghqrux|srsr=5000|nsfnv=1|reportUrl=https://bf99293tkn.bf.dynatrace.com/bf|srvr=%5C%2Fidentity%5C%2Fsettings|rdnt=2|uxrgce=1|cuc=t0rtn87t|mel=100000|dpvc=1|md=mdcc1=cfs_anid,mdcc2=bFS.attrs.cisId,mdcc3=bFS.User.profile.cisId,mdcc4=fx-ratelimit-remaining,mdcc5=bs.campaign,mdcc6=bs.channel,mdcc7=bs.pageName,mdcc8=bs.pageURL,mdcc9=bs.pageType,mdcc10=bs.visitorID|lastModification=1786000244457|tp=500,50,0|srbbv=2|agentUri=https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA15789NPRTUVXfghqrux_10343260715123811.js', + prod: 'app=a8e5edd77f861ace|cors=1|owasp=1|featureHash=ICA7NQVfghqrux|msl=153600|srsr=10000|vcx=1500|nsfnv=1|reportUrl=https://bf99293tkn.bf.dynatrace.com/bf|srvr=%5C%2Fidentity%5C%2Fsettings|rdnt=2|uxrgce=1|vcit=8000|cuc=t0rtn87t|srms=2,2,1,|mdl=mdcc11=20|mel=100000|dpvc=1|md=mdcc1=cfs_anid,mdcc2=bFS.attrs.cisId,mdcc3=bFS.User.profile.cisId,mdcc4=bs.pageName,mdcc5=bs.pageType,mdcc6=bs.pageURL,mdcc7=bs.visitorID,mdcc8=bs.campaign,mdcc9=bs.channel,mdcc10=fx-ratelimit-remaining,mdcc11=bdocument.referrer,mdcc12=dutm_campaign,mdcc13=dutm_source,mdcc14=dutm_medium|lastModification=1786000244457|tp=500,50,0|srbbv=2|agentUri=https://js-cdn.dynatrace.com/jstag/15c157a40ab/sri/ruxitagent_ICA7NQVfghqrux_10341260622154106.js' } // New RUM JavaScript tag (combined code+config _complete.js) from the Dynatrace global CDN. @@ -48,63 +57,66 @@ if (typeof getFeatureFlag !== 'undefined') { prod: 'https://js-cdn.dynatrace.com/jstag/15c157a40ab/bf99293tkn/a8e5edd77f861ace_complete.js' } - // Old-RUM inline bootstrap include path: a small sync stub that begins capture, then async-loads - // the full library. The new RUM has no equivalent small-bootstrap format, so this include is used - // for old RUM only. (The agent JS must NOT be run through EJS include() β€” see below.) - const inlinePathOld = './partials/dynatrace/_inline_' + env; - - // NOTE: the new RUM is loaded exclusively via script tags built from cdnUrlsNew / cdnConfigNew / - // cdnIntegrityNew / cdnCompleteUrlsNew. We deliberately do NOT include() the inline agent for the - // new RUM: EJS compiles included files as templates, and the new agent's minified JS contains the - // two-character EJS open-delimiter sequence (a less-than sign immediately followed by a percent + // The agent is never inlined: EJS compiles included files as templates, and the minified agent + // contains the EJS open-delimiter sequence (a less-than sign immediately followed by a percent // sign) inside a character-class string, which EJS misreads as an unterminated scriptlet and - // fails with "Could not find matching close tag". Emitting a script tag sidesteps that entirely. - if (dynatraceFlag.treatment === 'asyncCS-script') { %> - <% if (useNewRUM) { %> - - - <% } else { %> - - - <% } %> - <% } else if (dynatraceFlag.treatment === 'asyncCS-inline') { %> - <% if (useNewRUM) { %> - + // fails with "Could not find matching close tag". It would also re-ship 300-460 KB per response. + if (!rumDisabled) { + if (useSriSync) { %> + <% } else { %> - - <%- include(inlinePathOld) %> - <% } %> - <% } else if (dynatraceFlag.treatment === 'global-cdn') { %> - <% if (useNewRUM) { %> - + - <% } else { %> - - - <% } %> - <% } %> + <% } + } %> <% } %> diff --git a/packages/react-scripts/layout/views/partials/dynatrace/_inline_beta.ejs b/packages/react-scripts/layout/views/partials/dynatrace/_inline_beta.ejs deleted file mode 100644 index 34f8961ae23..00000000000 --- a/packages/react-scripts/layout/views/partials/dynatrace/_inline_beta.ejs +++ /dev/null @@ -1,73 +0,0 @@ - \ No newline at end of file diff --git a/packages/react-scripts/layout/views/partials/dynatrace/_inline_int.ejs b/packages/react-scripts/layout/views/partials/dynatrace/_inline_int.ejs deleted file mode 100644 index 731df536373..00000000000 --- a/packages/react-scripts/layout/views/partials/dynatrace/_inline_int.ejs +++ /dev/null @@ -1,73 +0,0 @@ - \ No newline at end of file diff --git a/packages/react-scripts/layout/views/partials/dynatrace/_inline_prod.ejs b/packages/react-scripts/layout/views/partials/dynatrace/_inline_prod.ejs deleted file mode 100644 index 204607df5bf..00000000000 --- a/packages/react-scripts/layout/views/partials/dynatrace/_inline_prod.ejs +++ /dev/null @@ -1,73 +0,0 @@ - From 0cf9e0e5f68bc77c9d535b3a9e7c4ea1a60b195e Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Sat, 8 Aug 2026 13:59:22 -0600 Subject: [PATCH 36/43] chore(rum): move maintainer tooling out of the published package scripts/ is for scripts consumers of @fs/react-scripts run (build, start, test, init, eject). The Dynatrace fetch script, its config artifact, and the RUM docs are maintainer-only -- the fetch script needs a Dynatrace API token, AWS credentials and keychain access, and is documented as being run from a checkout rather than from node_modules. Because "scripts" is in package.json `files`, all of it was being published. Moving it to tools/, which is not in `files`, excludes it by construction -- no negation patterns to remember when adding future maintainer material. The fetch script also carried internal infrastructure details into the published artifact: the S3 bucket, the tenant URL, keychain service names, and the three APPLICATION-* entity IDs. Published package: 119 files / 392 KB -> 104 files / 247 KB, combined with the _inline_*.ejs removal in the previous commit. The only Dynatrace file still shipping is dynatrace.ejs, which consumers actually render. Also here: - Add DYNATRACE_RUM_MECHANISMS.md -- the option matrix, the candidates that were evaluated and rejected (inline bootstrap, self-hosted agent, auto-injection) with the case for each and why it lost, the decision rationale, revisit triggers, and the revival recipe. - Add beacon-harness/ with the retained raw JSONL from the run that measured New RUM's ~2.3x per-session upload increase. Standalone: its package.json is not picked up by the workspace, whose glob is packages/* (direct children only). - Fix paths broken by the move: the fetch script's relative path to dynatrace.ejs, and the doc cross-links. - Rewrite the unreleased 8.17.0 changelog entry, which described the superseded dual-flag design. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG-FRONTIER.md | 20 +- packages/react-scripts/package.json | 2 +- .../scripts/dynatrace-rum-config.json | 23 -- .../dynatrace/DYNATRACE_RUM_MECHANISMS.md | 321 ++++++++++++++++++ .../dynatrace}/DYNATRACE_RUM_UPDATE.md | 63 ++-- .../tools/dynatrace/beacon-harness/README.md | 88 +++++ .../dynatrace/beacon-harness/analyze.mjs | 75 ++++ .../beacon-harness/data/v2-beta.jsonl | 15 + .../beacon-harness/data/v2-int.jsonl | 40 +++ .../beacon-harness/data/v2-prod.jsonl | 15 + .../beacon-harness/measure-beacons.mjs | 101 ++++++ .../dynatrace/beacon-harness/package.json | 15 + .../beacon-harness/probe-channels.mjs | 59 ++++ .../tools/dynatrace/dynatrace-rum-config.json | 23 ++ .../dynatrace}/fetch-dynatrace-scripts.js | 4 +- 15 files changed, 798 insertions(+), 66 deletions(-) delete mode 100644 packages/react-scripts/scripts/dynatrace-rum-config.json create mode 100644 packages/react-scripts/tools/dynatrace/DYNATRACE_RUM_MECHANISMS.md rename packages/react-scripts/{scripts => tools/dynatrace}/DYNATRACE_RUM_UPDATE.md (61%) create mode 100644 packages/react-scripts/tools/dynatrace/beacon-harness/README.md create mode 100644 packages/react-scripts/tools/dynatrace/beacon-harness/analyze.mjs create mode 100644 packages/react-scripts/tools/dynatrace/beacon-harness/data/v2-beta.jsonl create mode 100644 packages/react-scripts/tools/dynatrace/beacon-harness/data/v2-int.jsonl create mode 100644 packages/react-scripts/tools/dynatrace/beacon-harness/data/v2-prod.jsonl create mode 100644 packages/react-scripts/tools/dynatrace/beacon-harness/measure-beacons.mjs create mode 100644 packages/react-scripts/tools/dynatrace/beacon-harness/package.json create mode 100644 packages/react-scripts/tools/dynatrace/beacon-harness/probe-channels.mjs create mode 100644 packages/react-scripts/tools/dynatrace/dynatrace-rum-config.json rename packages/react-scripts/{scripts => tools/dynatrace}/fetch-dynatrace-scripts.js (98%) diff --git a/CHANGELOG-FRONTIER.md b/CHANGELOG-FRONTIER.md index 143213a6f1d..dd6075a0adf 100644 --- a/CHANGELOG-FRONTIER.md +++ b/CHANGELOG-FRONTIER.md @@ -1,16 +1,14 @@ ## 8.17.0 -- Upgrade Dynatrace RUM agent to version 1.329+ and enable new RUM experience - - Added feature flag `frontier_snow_dynatraceNewRUM` to control RUM version selection (gradual rollout); the existing `frontier_snow_dynatraceRUM` flag still selects the loading mechanism - - New RUM is loaded exclusively via ` - <% } - } %> + <% } %> -<% } %> + <% } +} %> diff --git a/packages/react-scripts/tools/dynatrace/EARLY_ERROR_BUFFER.md b/packages/react-scripts/tools/dynatrace/EARLY_ERROR_BUFFER.md new file mode 100644 index 00000000000..de98508c9cf --- /dev/null +++ b/packages/react-scripts/tools/dynatrace/EARLY_ERROR_BUFFER.md @@ -0,0 +1,131 @@ +# Early-error buffer + +A ~20-line inline listener in `` that catches errors thrown before the RUM agent exists and +replays them once it arrives. Lives in +[dynatrace.ejs](../../layout/views/partials/dynatrace.ejs), directly above `dtWhenReady`. + +## Why it exists + +Loading the agent `async` means its error handlers are not installed at navigation start. Measured +on int with [beacon-harness/blind-window.mjs](https://github.com/fs-webdev/create-react-app/blob/rum-harness-archive-2026-08-17/packages/react-scripts/tools/dynatrace/beacon-harness/blind-window.mjs), n=10: + +| offset | XHR captured | Error captured | +| --- | --- | --- | +| 0 ms | 10/10 | **0/10** | +| 100 ms | 10/10 | **0/10** | +| 250 ms | 10/10 | 2/10 | +| 400 ms | 10/10 | 5/10 | +| 550 ms | 10/10 | 10/10 | +| 700 ms + | 10/10 | 10/10 | + +Requests are never lost β€” the agent recovers them retroactively from Resource Timing. **Exceptions +are the only thing the async load actually loses**, because there is no buffered API for an error +thrown with no listener attached. Once it is gone, it is gone. + +That is a narrow loss, but a badly-placed one: errors in the first half-second are +page-load errors, which are the ones you most want to see. + +## Sentry does not cover this + +The obvious question is whether Sentry catches what Dynatrace misses. It does not. Measured on int, +median of 4 runs: + +| | Installed at | +| --- | --- | +| `window.dtrum` / `window.dynatrace` | **660 ms** | +| `window.__SENTRY__` | **1016 ms** | + +Sentry initialises from the app bundle, which is larger and later than the 115 KB agent, so it +shares the blind window and extends it by ~356 ms. Anything Dynatrace misses, Sentry misses too. + +Also worth knowing: `SERVER_DATA.sentryDSN` is **empty on int and beta** β€” set only in prod. The SDK +loads and costs bundle weight in the lower environments but transports nothing, so Sentry error +coverage cannot be validated there at all. + +The gap is a property of installing a handler late, not of any particular vendor. The only fix is a +listener that already exists at t=0. + +## How it works + +1. An inline IIFE registers `error` and `unhandledrejection` listeners in the **capture phase**, so + a `stopPropagation()` elsewhere cannot hide errors from it. Entries are pushed to + `window.__dtEarlyErrors` with a `performance.now()` stamp. Bounded at 50 β€” a page erroring in a + loop must not grow it without limit. +2. When the agent appears, the existing `dtWhenReady('classic', …)` callback stops the listeners + and replays the buffer through `dtrum.reportError`. + +It must stay **first** in that script block, and the partial must stay early in `` β€” anything +that throws before it runs is unrecoverable. It currently renders at `layout.ejs` line 42, ahead of +`SERVER_DATA` (52) and Sentry (65). + +The block is inside the `!rumDisabled` branch, so the `off` treatment emits **zero bytes**. If RUM +is kill-switched because it is causing a problem, our listeners should not still be installed. + +## Why it reports through RUM Classic + +Verified against the live agent on int: + +| API | | +| --- | --- | +| `dtrum.reportError` | **function** | +| `dtrum.reportCustomError` | function | +| `dynatrace.reportError` | **undefined** | + +New RUM has no direct error-reporting method β€” it exposes `sendEvent`, whose error event shape we +have not verified. Classic runs alongside New RUM and cannot be disabled, so `dtrum.reportError` is +always available. + +**Open question:** whether an error reported through Classic also surfaces in Grail (`user.events`). +Classic and Grail are separate ingest channels. `characteristics.is_api_reported` does appear on +Grail events, so API-reported data reaches it in general, but that has not been confirmed for +`reportError` specifically. If it does not cross over, the replay is visible in Classic dashboards +only, and reaching Grail would need `dynatrace.sendEvent` with a hand-built error event. + +## Known imprecisions, accepted deliberately + +**Double-reporting in a ~100 ms sliver.** The agent installs its handlers slightly *before* +`window.dtrum` becomes visible β€” measured, errors were captured from 550 ms while `dtrum` appeared +at ~660 ms. An error thrown in that gap can be caught by both the agent and the buffer. Duplicating +a handful of errors is the better failure than dropping them; replayed entries carry a +`[dt-early +ms]` prefix so they can be excluded if it ever matters. + +**Timing attribution shifts.** A replayed error is reported when the agent arrives, not when it was +thrown. The original offset is preserved in the message prefix. + +**Stack fidelity.** The replay passes the original `Error` object where one exists, so the real +stack survives. Where only a message is available it constructs a new `Error`, and the agent +synthesises a stack pointing at the replay site rather than the throw. `exception.is_stack_trace_generated` +in `user.events` distinguishes the two. + +## Verifying it works + +The same harness that measured the gap proves the fix. After deploying, re-run: + +```bash +cd packages/react-scripts/tools/dynatrace/beacon-harness +node blind-window.mjs int 10 "0,100,250,400,550,700,850,1000,1500" +``` + +The **Error captured** column should go from `0/10` at t=0 to `10/10` at every offset. Anything less +means the buffer is installing too late, the replay is not firing, or `reportError` is not reaching +the beacon. + +To find replayed errors in Grail: + +``` +fetch user.events, from:-2h +| filter characteristics.has_error == true +| filter contains(exception.message, "dt-early") +| fields timestamp, exception.message, exception.type, error.source, dt.rum.session.id +| sort timestamp asc +``` + +An empty result with a passing harness run would answer the open question above β€” reported to +Classic, not crossing to Grail. + +## What this changes about the mechanism decision + +The sync SRI arm was retained partly as the hedge against async losing early errors. If the buffer +works, that half of its justification goes away and only the caching argument remains β€” which is +itself contingent on Dynatrace declining the `ETag` request. See +[DYNATRACE_RUM_MECHANISMS.md](./DYNATRACE_RUM_MECHANISMS.md). From 436a270f4c015a58e2f9970b12494830bca3ebdf Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Mon, 17 Aug 2026 20:49:25 -0600 Subject: [PATCH 41/43] docs(rum): record how chunk-load failures reach RUM ChunkLoadError does reach Grail, complete with message, type and a real stack trace -- but via error.source: console, not window.onerror. RetryChunkLoadPlugin catches the rejection itself and reports the exhausted retry through console.error. The early-error buffer listens for error and unhandledrejection, so it does not cover this; documented as a deliberate gap rather than closed, since buffering console.error risks double-reporting every console error on the page. Two measurement traps recorded so they are not repeated: the retry plugin appends ?cache-bust=, so a Playwright pattern ending at .chunk.js lets all five retries through and the run reads as no error at all; and blocking every chunk stops the app booting, so no import() rejection handler runs and no ChunkLoadError is ever constructed. Also confirms page.route() disables Chromium's HTTP cache even for a narrow pattern (warm agent transferSize 175256 vs 0), so timings from that harness are a conservative bound. Harness archived at tag rum-harness-archive-2026-08-17. Co-Authored-By: Claude Opus 5 (1M context) --- .../tools/dynatrace/EARLY_ERROR_BUFFER.md | 23 ++++++ .../tools/dynatrace/RUM_ERROR_PROBE_SPEC.md | 76 ++++++++++++++++++- 2 files changed, 96 insertions(+), 3 deletions(-) diff --git a/packages/react-scripts/tools/dynatrace/EARLY_ERROR_BUFFER.md b/packages/react-scripts/tools/dynatrace/EARLY_ERROR_BUFFER.md index de98508c9cf..ae03dafc07f 100644 --- a/packages/react-scripts/tools/dynatrace/EARLY_ERROR_BUFFER.md +++ b/packages/react-scripts/tools/dynatrace/EARLY_ERROR_BUFFER.md @@ -81,6 +81,29 @@ Grail events, so API-reported data reaches it in general, but that has not been `reportError` specifically. If it does not cross over, the replay is visible in Classic dashboards only, and reaching Grail would need `dynatrace.sendEvent` with a hand-built error event. +## Chunk-load failures are not covered + +The buffer listens for `error` and `unhandledrejection`. A chunk-load failure reaches RUM through +neither: `RetryChunkLoadPlugin` catches the rejection itself and reports the exhausted retry with +`console.error`, which the agent picks up by patching the console. Measured with +[beacon-harness/chunk-failure.mjs](https://github.com/fs-webdev/create-react-app/blob/rum-harness-archive-2026-08-17/packages/react-scripts/tools/dynatrace/beacon-harness/chunk-failure.mjs), the event arrives as +`exception.type: ChunkLoadError` with `error.source: "console"` β€” see +[RUM_ERROR_PROBE_SPEC.md](./RUM_ERROR_PROBE_SPEC.md#answered-chunk-load). + +So a chunk failure inside the blind window would be lost, and this buffer would not save it. + +In practice that is currently fine: on int, chunks are requested ~850–1200 ms and the error lands +~1150 ms, while the agent is ready at ~500–930 ms. The failure sits outside the window. But the +ranges overlap, so a slow enough connection inverts the order. + +Closing it would mean also buffering `console.error`, which is a bigger change than it looks β€” +the console is patched by the agent too, so the buffer would have to hand back cleanly to avoid +double-reporting every console error on the page, not just those in the first half-second. That +is why it was not done here. **Open, deliberately deferred**; revisit if chunk errors turn out to +be under-counted in the field relative to the failed-request count, which is the signal that +would show it (the failed chunk URL always reaches RUM via Resource Timing, 5/5, even when the +error itself does not). + ## Known imprecisions, accepted deliberately **Double-reporting in a ~100 ms sliver.** The agent installs its handlers slightly *before* diff --git a/packages/react-scripts/tools/dynatrace/RUM_ERROR_PROBE_SPEC.md b/packages/react-scripts/tools/dynatrace/RUM_ERROR_PROBE_SPEC.md index c28fa9382cc..b2b94d5011c 100644 --- a/packages/react-scripts/tools/dynatrace/RUM_ERROR_PROBE_SPEC.md +++ b/packages/react-scripts/tools/dynatrace/RUM_ERROR_PROBE_SPEC.md @@ -73,7 +73,7 @@ Suggested sweep for `at`: `0, 100, 250, 400, 550, 700, 1000` β€” brackets the me | `reject` | `Promise.reject(new Error(marker))`, unhandled | `unhandledrejection` listener | Likely same as `throw` β€” separate listener, same install timing | | `console` | `console.error(marker)` | console patching | Likely same. **Confirmed capturable** β€” `error.source: "console"` accounts for 30 of 31 beta errors | | `module-eval` | `throw` at the top of the main bundle, before React | `window.onerror` | **Unknown** β€” bundle vs agent race | -| `chunk-load` | `import()` a deliberately-missing chunk | `onerror` **and** Resource Timing | **Unknown, most interesting** β€” may be recovered retroactively like XHR | +| `chunk-load` | `import()` a deliberately-missing chunk | **console** and Resource Timing | **ANSWERED** β€” see below; no longer needs a FAR probe | | `resource-404` | inject `` | element `error` event + Resource Timing | Likely recovered | | `xhr-500` | `fetch()` a route returning 500 | monkey-patch + Resource Timing | Recovered β€” already confirmed | | `react-render` | throw during initial component render | `window.onerror` | Always captured β€” **control case**, proves the probe works | @@ -186,6 +186,76 @@ is fine for this purpose and actively good for keeping beta clean. | When is the agent ready? | median 689 ms, range 472–808 ms (fast connection) | same | | Does the error handler install before `window.dtrum` appears? | Yes β€” errors captured at 550 ms while `dtrum` appeared at ~689 ms | same | | Is `console.error` captured? | Yes, as `error.source: "console"` | live beta data | +| Are chunk-load failures captured? | Yes β€” as `exception.type: ChunkLoadError` via **`error.source: console`** | `chunk-failure.mjs`, n=5 | +| Does the failed chunk URL reach RUM? | Yes, 5/5, recovered from Resource Timing | same | -The new information this probe adds is: **chunk-load and resource failures**, **module-eval -timing**, and whether any of it changes on a real device or from another region. +The new information this probe adds is: **module-eval timing**, real-device and cross-region +behaviour, and error shapes the harness cannot synthesise. The `chunk-load` case below is done. + +--- + +## Answered: `chunk-load` + +[beacon-harness/chunk-failure.mjs](https://github.com/fs-webdev/create-react-app/blob/rum-harness-archive-2026-08-17/packages/react-scripts/tools/dynatrace/beacon-harness/chunk-failure.mjs) answers this against int +without any change to frontier-app-react β€” Playwright aborts the request and the app produces a +genuine failure. Measured n=5, agent live at throw time in 5/5: + +| Signal | Result | +| --- | --- | +| `exception.type: ChunkLoadError` in beacon | **5/5** | +| Failed chunk URL in beacon | 5/5 (Resource Timing) | +| `error.source` | **`console`**, not `exception` | + +The event is complete β€” message, type, and a real stack trace: + +```json +"exception.message": "Loading chunk 6285 failed after 5 retries.\n(static/js/6285.6f792c54f5f8f880.chunk.js)", +"exception.type": "ChunkLoadError", +"error.source": "console", +"exception.stack_trace": "ChunkLoadError\n at o.f.j (…/main.833f3be69ff336a0.js:1:190173)…" +``` + +**It arrives via console patching**, which matters more than it sounds β€” see +[EARLY_ERROR_BUFFER.md](./EARLY_ERROR_BUFFER.md#chunk-load-failures-are-not-covered). +A `window`-level `error`/`unhandledrejection` listener never sees it: `RetryChunkLoadPlugin` +catches the rejection itself and reports the exhausted retry through `console.error`. + +Timing on int: the chunk is requested ~850–1200 ms and the error lands ~1150 ms, i.e. **after** +the agent (~500–930 ms). Real chunk failures therefore sit outside the blind window in normal +conditions, though the two ranges overlap enough that a slow connection could invert it. + +The URL recovery is genuine and separable from the error text β€” the beacon carries standalone +resource entries, one per attempt including all five retries: + +```json +"performance.initiator_type":"script","characteristics.has_request":true, +"url.full":"https://edge.fscdn.org/assets/static/js/7502.e0cb07ce009ca5b5.chunk.js" +``` + +### Unresolved: blocking `main.js` produces nothing at all + +Mode `early` blocks the main bundle instead of a lazy chunk. Result (n=4): **no error, no failed +URL, nothing** β€” including in the 2 of 4 runs where the agent was already live when the request +failed. That is not what the chunk result predicts. + +Not yet explained, and the sample is too small to lean on. Candidates: the session was a +`costAndTrafficControl` stub (~1 in 3 are monitored, so 0/4 is unremarkable); or a page whose +bundle never loads generates too little activity for the agent to flush a full beacon β€” it sent +only 2. Worth an n=15 run before drawing anything from it. **If it holds, it matters**: it would +mean the hardest failure to detect is the one where the app does not start at all. + +### Two mistakes worth not repeating + +**Match the retry query string.** `RetryChunkLoadPlugin` is configured with `maxRetries: 5` and a +`?cache-bust=` query ([config/webpack.config.js:827](../../config/webpack.config.js#L827)), +so a Playwright pattern ending at `.chunk.js` blocks the first attempt and lets all five retries +through. They succeed, the app recovers, and the run reads as "no error was ever produced". The +pattern must end `*.chunk.js*` and the per-chunk lock must compare query-stripped URLs. + +Also worth knowing on its own: the five retries fire **within ~23 ms** of the first failure +(`retryDelay` defaults to 0), so they do nothing for a CDN outage or a genuinely slow network β€” +the case the plugin was added for. Raising `retryDelay` is a separate, real finding. + +**Do not block every chunk.** Blocking all 19 stops the app booting, so no `import()` rejection +handler ever runs: measured 19 resource errors, **zero** `ChunkLoadError`, zero retries. Only a +single-chunk block reproduces the real failure. From 585a355696ee84dcf8540c2bdd30109ea61199e1 Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Mon, 17 Aug 2026 20:49:26 -0600 Subject: [PATCH 42/43] docs(rum): record which errors survive the build, and why prod cannot debug them Two families, split by whether webpack lets them through. Delivery failures: a flaky network the retry plugin recovers from produces NO RUM error at all -- 0/3 ChunkLoadError -- and is visible only as failed resource requests. Sizing flaky-network impact from error counts will read zero. Errors that survive the build: syntax errors and statically-resolvable missing imports fail the build and never ship, so simulating them measures a case that cannot happen. What does ship is runtime failure, and on int it is largely undebuggable: typeerror -> "Script error.", no message, no file, no line handler -> "Script error.", agent fully live, still opaque promise -> nothing at all missing-module -> nothing at all string-typo -> nothing; nothing throws (expected) logic -> nothing; nothing throws (control) Cause is one missing attribute: chunk