From fde974365fcb5a9cd5fcce862deed531da9c752a Mon Sep 17 00:00:00 2001 From: Logan Allred Date: Tue, 26 May 2026 14:06:42 -0600 Subject: [PATCH 01/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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 `