From a82aaeff2b177ff424b062e24b2478436db8a182 Mon Sep 17 00:00:00 2001 From: Bhargav Date: Thu, 5 Nov 2026 07:08:19 +0000 Subject: [PATCH] fix(rum): fix session link NaN, heatmap snapshot, trace duration, widen button and analytics header (#16389) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- scripts/translations/.translation_state.json | 109 +++- .../ci-matrix/ci_matrix_regression.json | 7 +- .../rum-3268-analytics-header-1200.spec.js | 140 ++++++ .../RUM/rum-3268-heatmap-no-view-id.spec.js | 92 ++++ .../RUM/rum-3268-no-widen-at-30d.spec.js | 52 ++ .../RUM/rum-3268-session-link-no-nan.spec.js | 99 ++++ .../RUM/rum-3268-trace-duration-skew.spec.js | 217 ++++++++ .../components/rum/PlayerTracesTab.spec.ts | 62 +++ web/src/components/rum/PlayerTracesTab.vue | 25 +- .../productAnalytics/AnalyticsFunnels.spec.ts | 17 + .../rum/productAnalytics/AnalyticsFunnels.vue | 1 + .../AnalyticsOverview.spec.ts | 22 + .../productAnalytics/AnalyticsOverview.vue | 1 + .../productAnalytics/AnalyticsPaths.spec.ts | 31 ++ .../rum/productAnalytics/AnalyticsPaths.vue | 2 + .../AnalyticsRetention.spec.ts | 16 + .../productAnalytics/AnalyticsRetention.vue | 2 +- .../rum/useProductAnalytics.spec.ts | 52 ++ .../composables/rum/useProductAnalytics.ts | 14 + web/src/composables/rum/useSessionHeatmap.ts | 2 +- web/src/locales/languages/ar-SA.json | 118 ++++- web/src/locales/languages/de-DE.json | 118 ++++- web/src/locales/languages/en-US.json | 1 + web/src/locales/languages/es-ES.json | 118 ++++- web/src/locales/languages/fr-FR.json | 118 ++++- web/src/locales/languages/it-IT.json | 118 ++++- web/src/locales/languages/ja-JP.json | 118 ++++- web/src/locales/languages/ko-KR.json | 118 ++++- web/src/locales/languages/nl-NL.json | 118 ++++- web/src/locales/languages/pl-PL.json | 118 ++++- web/src/locales/languages/pt-PT.json | 118 ++++- web/src/locales/languages/ru-RU.json | 118 ++++- web/src/locales/languages/th-TH.json | 118 ++++- web/src/locales/languages/tr-TR.json | 118 ++++- web/src/locales/languages/vi-VN.json | 118 ++++- web/src/locales/languages/zh-CN.json | 118 ++++- web/src/locales/languages/zh-TW.json | 118 ++++- web/src/views/RUM/AppAnalytics.spec.ts | 88 +++- web/src/views/RUM/AppAnalytics.vue | 464 ++++++++++-------- web/src/views/RUM/AppSessions.spec.ts | 80 ++- web/src/views/RUM/AppSessions.vue | 20 +- web/src/views/RUM/ClickHeatmap.spec.ts | 49 +- web/src/views/RUM/ClickHeatmap.vue | 50 +- web/src/views/RUM/SessionViewer.spec.ts | 32 ++ web/src/views/RUM/SessionViewer.vue | 4 + 45 files changed, 3321 insertions(+), 318 deletions(-) create mode 100644 tests/ui-testing/playwright-tests/RegressionSet/RUM/rum-3268-analytics-header-1200.spec.js create mode 100644 tests/ui-testing/playwright-tests/RegressionSet/RUM/rum-3268-heatmap-no-view-id.spec.js create mode 100644 tests/ui-testing/playwright-tests/RegressionSet/RUM/rum-3268-no-widen-at-30d.spec.js create mode 100644 tests/ui-testing/playwright-tests/RegressionSet/RUM/rum-3268-session-link-no-nan.spec.js create mode 100644 tests/ui-testing/playwright-tests/RegressionSet/RUM/rum-3268-trace-duration-skew.spec.js diff --git a/scripts/translations/.translation_state.json b/scripts/translations/.translation_state.json index 1672780a83..6a72c114ad 100644 --- a/scripts/translations/.translation_state.json +++ b/scripts/translations/.translation_state.json @@ -2995,6 +2995,11 @@ "organizationHelpText": "d9cdc834fbfc24d8", "organizationLabel": "d764d42592fc9687", "output_format": "ef22a51c869f0983", + "payloadFormat": "20d3e5624cadea6b", + "payloadFormatAuto": "d461a493a3753877", + "payloadFormatHelp": "39611037b3f91112", + "payloadFormatLegacy": "46a69d0863b7b935", + "payloadFormatRendered": "61954406a964a6df", "prebuilt": { "chatPostsShort": "d22f97f025061bc3", "customShort": "3ff61f1bbc91d71e", @@ -3294,6 +3299,14 @@ "addTitle": "30d87ec4525974fb", "addToTemplate": "1db08760ac2ebd31", "addToTemplateCaption": "f83de3e69ecc3f94", + "allKindsNotice": "7f23c044c629633d", + "allKindsNoticeReview": "aff0766a5290e117", + "appliesTo": "6687458beee57a58", + "appliesToAll": "ddd0c2108e40c8b8", + "appliesToAny": "bcb32f115ce6b582", + "appliesToInferred": "a5e37fb769ee7dc8", + "appliesToManual": "e4b802b7ff239951", + "appliesToReset": "c5322f1451f4890c", "body": "6ccaa6415b5ee449", "bodyInvalidJsonHelp": "3c676f355a9f94f6", "bodyMarkdownLint": "5e13130e86487d50", @@ -3346,6 +3359,7 @@ "emojiFire": "c141faa80e43a119", "emojiSiren": "248f1ca4059ee2a7", "emojiWarning": "e981ddae45d8f4ca", + "emptyVariables": "fc839fc6f5b02a31", "exportTemplate": "e2fcdf940448a5f5", "fieldLabel": "0e66373f45dcf3dd", "fieldLabelPlaceholder": "1be972b2422a73f1", @@ -3390,16 +3404,20 @@ "nameLockedHint": "3bb173a8b2c6bfe8", "namePlaceholder": "42a4c305cb8432c1", "outputMessages": "ace70a9ce0d04504", + "playgroundHelp": "41159d362d20d3b0", + "playgroundLabel": "a6273fb551f223ce", "prebuiltBadge": "e230325386e39e00", "prebuiltBadgeHint": "39e7b4153d331e0e", "previewApproximate": "324b134f57c70c72", "previewChannel": "ce4683e7013a18cd", "previewEmptyTitle": "0a44b1414f67ff5d", + "previewFailed": "278e5c0835162cd8", "previewFidelityEmail": "6453bcfab90d9306", "previewFidelityMachine": "47ad920f68131a8b", "previewFidelityNote": "b2ae97d334ae9e61", "previewFidelityWebhook": "8790c72363c615c7", - "previewGroupLabel": "f1a4aba767ae014e", + "previewForbidden": "2755a8230f22b63c", + "previewKind": "f5387f9bb6ed7031", "previewRawPayload": "99733344956dde48", "previewSeverity": "5e9f98120dbe5682", "previewSeveritySingle": "2b2d13f4dee205db", @@ -3414,8 +3432,12 @@ "rowsSectionDescription": "0890b041ad6db134", "savedSuccessfully": "b2c655467ea71e28", "severityCritical": "427dd2969bd140be", + "severityDegraded": "a8494c12f2243903", + "severityFailed": "031a8f0f659df890", + "severityFlaky": "cefa8e8972de2695", "severityNoData": "3b41ba9c7cb8c5d6", "severityOk": "843ac01149cced78", + "severityRecovered": "37ce0a91ee27ce92", "severityWarning": "e981ddae45d8f4ca", "showWhenAllPlaceholder": "a52ace420f2175d0", "showWhenCaption": "e14d545f76326843", @@ -3442,6 +3464,7 @@ "unknownVariables": "9a870420a8e0846b", "updateTitle": "cdf3d76a7834d5c5", "variableEpisodeIdDescription": "675c8fcb2fd2c8ec", + "variableGroupCommon": "309955e00850b8af", "variableLimits": "6cbac2ac16a05eaa", "variableRowsDescription": "a14c93fab7fc021c", "variableStreamFields": "e86ad015c050bbe6", @@ -3563,14 +3586,18 @@ "createsIncidentTooltip": "6247680d29ce5226", "destinationsTooltip": "f601ce64da7a4c20", "helpApplyToAlert": "b260517401d76a07", + "helpApplyToObject": "1a439ee577c7ee95", "helpBuiltInFooter": "e9a6a4220e72dad6", "helpBuiltInHeading": "9dc1591f15a114a9", "helpBuiltInIntro": "57600ff308a84234", "helpCurrentBodyEmpty": "9b1b8cde966d919a", "helpCurrentHeading": "f9c8e6c12fcd5c88", + "helpCurrentHeadingObject": "6e8bab1852eaef29", "helpCurrentNoDestinations": "8611ded262fceb3e", "helpCurrentNoOverride": "00465ee58ba181a6", "helpCurrentOverrideHeading": "2cef85e7cce184f7", + "helpCurrentOverrideSome": "7014110c33ae2ff8", + "helpCurrentResolved": "aaf94304ecd4acb7", "helpDestinationNoTemplate": "a90b2dde07be7449", "helpExampleHeading": "d029f87e3d80f8fd", "helpExampleResultLabel": "e9b57b6fd33d498b", @@ -3597,8 +3624,10 @@ "helpRowTemplateWhyDesc": "9526c74b34dda66d", "helpRowTemplateWhyHeading": "268e600b3182e877", "helpTemplateExplain": "4c1506ff2ab22fad", + "helpTemplateExplainObject": "55662c5336e0d905", "helpTemplateTitle": "84de89c26b0ff73f", "helpTemplateWhenDesc": "ee28cd7620178bb7", + "helpTemplateWhenDescObject": "f6279efe0a37f7fe", "helpTemplateWhenHeading": "c38334b43ce34521", "helpVariablesExampleCaption": "dfebcf401828f373", "helpVariablesExplain": "b8d86c46c9ea8212", @@ -4299,6 +4328,7 @@ "ok": "c16bc82bf1f04ede", "pending": "e595f17fac1d8384", "result": "6e7d50e84f4731ef", + "template": "0575f29df888a27e", "time": "33b93476cf597a33" }, "regionNote": "f5ed2e60103bf71c", @@ -5404,6 +5434,14 @@ "noData": "af75117dfbb0edb7", "noTemplatesMsg": "e9e871d4bebbbe00", "noTriggersLoaded": "980bb294c56dd9e3", + "notificationKinds": { + "alert": "44a57b22e03d9a66", + "anomaly": "79622828d2d8d988", + "downtime": "5f8d6d8feb7b17a5", + "incident": "36a606d4886552e8", + "on_call": "0a33491906e00678", + "synthetics": "f45c092981915635" + }, "oncallTeam": "b250e6b0b87af388", "oncallTeamAtFireTime": "80a154924b2e1c49", "oncallTeamFromRules": "6ca74cdb253f8fc9", @@ -5907,6 +5945,34 @@ "tags": "1331275bc537b4c2", "tagsTooltip": "a56e372b90037999", "template": "84de89c26b0ff73f", + "templateOverride": { + "builtIn": "f721f7df7e39f9f3", + "create": "7dc9c0adaca09e51", + "emailOnly": "ecf8fea0cb44b2e3", + "empty": "a1f60de70abec9c9", + "forbidden": "e15b5db2103d2103", + "groupContent": "47bd29075f8b8019", + "groupCustom": "494ca78f7374e46f", + "help": "ec7761ee7c813c65", + "label": "3037bb192c8121ac", + "loadFailed": "9a794687fa935a04", + "mismatch": "c2d86508bc207eab", + "placeholder": "c3033736b3864b6d", + "snsOnly": "a97427f6124e7e52", + "sourceCompiled": "33a7431aadae8522", + "sourceDestination": "09849c42b87f4092", + "sourceExplicit": "2995c0d322bbce95", + "sourceLegacy": "57c686be41c66320", + "sourceLegacyPayload": "278fc723fb4fcdb5", + "sourceOrgDefault": "cbf6d49ec0f6d5bb", + "sourcePlatformResolve": "ebb6b76a74129396", + "stale": "c66188ecd3e68b35", + "templateWithSource": "8fb8577722557a62", + "webhookOnly": "267c3107cccf89d6", + "willUse": "0c4145de85e3a7f9", + "willUsePerDestination": "e1f36c1e4129bef4", + "willUseRow": "13c699e5269fd408" + }, "testErrorClientError": "a68c0b2917185644", "testErrorConnection": "d756aae07e05473d", "testErrorDNS": "d3477f5ad3f618cb", @@ -15389,6 +15455,17 @@ "envButton": "8594d2ee749572c4", "envHelp": "a674c8b69df5e79c", "envTitle": "08d3da57e11a658c", + "health": { + "checkAgain": "fb7099ad8e818d42", + "checkAgainHint": "1d7f729465b86cd9", + "checking": "5e8de672881e0d07", + "error": "4a98aa4088c2ee4d", + "noGenAiSpans": "089cf5c60886e5cb", + "scopeAgent": "2ab32ec1ddbad9af", + "scopeStream": "be6e5739c9f5e7d8", + "title": "f918ce9614900771", + "unsupported": "fdb2f19aeabd3d77" + }, "promptCopied": "c55394776b1492cf", "promptLabel": "5c39123805ffb4e2", "promptTitle": "c36d206528dfdcd8", @@ -15548,6 +15625,7 @@ "connectSource": "0d50d7e94e3cddd3", "copyCommand": "9a01feecae675f2f", "copyConfig": "33ef5844548d0a4f", + "copyPrompt": "ffc64b8bb8c4532d", "elapsedMinutes": "1829c58ba472520f", "elapsedSeconds": "e4e6c0fbc1a49d45", "endpointLabel": "3df9726c68ba76d6", @@ -15555,19 +15633,25 @@ "fixInvalidCredentials": "14e39e6a9a1f1547", "fixInvalidCredentialsConfig": "7d023861b759a3a5", "fixInvalidCredentialsConfigUnnamed": "bcb719d88183cb95", + "fixInvalidCredentialsPrompt": "c2fb88cc64830cf8", + "fixInvalidCredentialsPromptUnnamed": "69da5571f9384541", "fixInvalidCredentialsUnnamed": "4d8976ca5087b52a", "fixMalformedBody": "5d360efdbe892460", "fixRateOrQuota": "bf6b0dcb2e6d2130", "guideDocs": "9d5099415bc97cb2", "hintStillChecking": "a49091733bfa9904", "hintWaiting": "c5136fb31e8de3a6", + "hintWaitingAfter": "b4d295041fd15738", "hintWaitingLate": "36a7512a71c70f0e", "idleIn": "9bd8f732bf90fe71", "idleInConfig": "654a3d3a666c47da", + "idleInPrompt": "e5ca7ab87f8e020e", "idleMatches": "07b128b00baed4cc", "idleMatchesConfig": "50da86325e8062d0", + "idleMatchesPrompt": "8bf54703aeb0fcf0", "idleNew": "9ad87a51f28cd79d", "idleNewConfig": "4c7fccd2b51a4c5f", + "idleNewPrompt": "ccd1f01c5b0c4426", "idlePill": "062093d7d3f4ff01", "installCheck": "64399266f1936bde", "meta": "2c6ef980667c5380", @@ -18275,6 +18359,7 @@ "deliveryStatus_delivered": "906115657390f367", "deliveryStatus_failed": "031a8f0f659df890", "deliveryStatus_sent": "c16bc82bf1f04ede", + "deliveryTemplate": "1ff1e821bde54da4", "deliveryTo": "f4b06ef6d3c81436", "description": "526e0087cc3f254d", "dimensionAlreadyUsed": "f45ee45ef04a4715", @@ -18706,6 +18791,7 @@ "policyFix": "21f1595b614ffc31", "policyHint": "8d8e8833d7ee31ae", "policyIfNoAck": "7a5eb9ed14493cc5", + "policyMailBanner": "f0232decc4aaab3d", "policyMainTabPolicy": "e1a8e3b1dca1c59a", "policyNoDefaultTeam": "ef87b2ed4b1bfa71", "policyNoDestinationWarning": "5a72c2c10ad7c3d8", @@ -23220,6 +23306,7 @@ "noPages": "225e96d078e9b6d7", "noPosition": "5c13bcfb8fce2a42", "noReplay": "392b83d55800274c", + "noReplayLegacySdk": "4c6e54c44430550f", "noTargets": "17c7a35194a02ab9", "offSnapshot": "9289a83bb31c8a0a", "opacityLabel": "2ddc54d254b4f59d", @@ -23450,11 +23537,31 @@ "platform": "c78ffe19571018fb", "playSessionReplay": "8859d8b3f9b8664f", "playerEvents": { + "abortedByApp": "e0de92dd7cca2b3d", "action": "64cff1319d2fd2cb", + "browserStatus": "5be4dfa46df153a7", + "duration": "4fc52a3c4c558b51", "error": "54a0e8c17ebb21a1", "failedRequest": "acc9650ce8c7cf32", + "failedRequestDetails": "90f2c85037d7a87f", "frustration": "8302925ce5f5d7fe", + "initiatedBy": "81b7f94aee9327fe", + "method": "52a0f9b65b278850", "noResponse": "961136f9207a19be", + "noResponseStatus": "e2e9c23acba97544", + "noServerSpanFound": "b89658e79d796e81", + "noServerSpanHint": "f8f83f8175a49272", + "serverStatus": "344c9097cc039597", + "size": "1af851907331c0ed", + "timing": "fc4e84255a41a3a2", + "timingConnect": "1a2303ede07493ac", + "timingDns": "020965a2f08f4028", + "timingDownload": "d6eafe8235910042", + "timingFirstByte": "da1f76e743e7a4d4", + "timingSsl": "07f9736e7adea312", + "unknown": "b764cdc0eab71374", + "unknownNoTrace": "26688553a94183a9", + "url": "e7a241debad56609", "view": "dcc839a4015c4b7d" }, "rageDeadClicks": "383f715434d129d3", diff --git a/tests/ui-testing/ci-matrix/ci_matrix_regression.json b/tests/ui-testing/ci-matrix/ci_matrix_regression.json index b08c4a4721..e601df833d 100644 --- a/tests/ui-testing/ci-matrix/ci_matrix_regression.json +++ b/tests/ui-testing/ci-matrix/ci_matrix_regression.json @@ -223,7 +223,12 @@ "rum-15190-paths-build-funnel.spec.js", "rum-15230-paths-drawer-user.spec.js", "rum-15304-overview-synthetic-counts.spec.js", - "rum-15269-list-view-state.spec.js" + "rum-15269-list-view-state.spec.js", + "rum-3268-session-link-no-nan.spec.js", + "rum-3268-heatmap-no-view-id.spec.js", + "rum-3268-trace-duration-skew.spec.js", + "rum-3268-no-widen-at-30d.spec.js", + "rum-3268-analytics-header-1200.spec.js" ], "disabled": [], "ingest_allowed_upto": "1440", diff --git a/tests/ui-testing/playwright-tests/RegressionSet/RUM/rum-3268-analytics-header-1200.spec.js b/tests/ui-testing/playwright-tests/RegressionSet/RUM/rum-3268-analytics-header-1200.spec.js new file mode 100644 index 0000000000..a09662f8c8 --- /dev/null +++ b/tests/ui-testing/playwright-tests/RegressionSet/RUM/rum-3268-analytics-header-1200.spec.js @@ -0,0 +1,140 @@ +// Product Analytics header: the title stays whole and clear of the app picker at 1440, 1366, 1290, 1200 and 1000px viewports; below ~1300px the filters wrap into their own band. + +const { test, expect } = require("../../utils/enhanced-baseFixtures.js"); +const testLogger = require("../../utils/test-logger.js"); +const PageManager = require("../../../pages/page-manager.js"); +const { + seedBuilt, + SessionBuilder, + runAppId, + HOUR_MS, +} = require("../../utils/rum-analytics-ingestion.js"); + +const TAGS = ["@bug-3268", "@P3", "@regression", "@rumRegression"]; +const SUB_TABS = ["funnels", "paths"]; +const WIDTHS = [1440, 1366, 1290, 1200, 1000]; +const WIDE_FROM = 1300; + +function buildViewSeed(appId, nowMs) { + const session = new SessionBuilder( + appId, + `${appId}-s0`, + nowMs - 2 * HOUR_MS, + { user: "h0@e2e.test" }, + ); + session.view("https://shop.example.com/web/start"); + session.view("https://shop.example.com/web/product"); + return { rows: session.rows, facts: { appId } }; +} + +test.describe("Product Analytics header across widths", () => { + test.describe.configure({ mode: "parallel" }); + test.use({ timezoneId: "UTC", viewport: { width: 1200, height: 800 } }); + let seed = null; + + test.beforeAll(async ({ browser }) => { + const page = await browser.newPage(); + try { + seed = await seedBuilt( + page, + buildViewSeed(runAppId("pa-3268"), Date.now()), + ); + } finally { + await page.close(); + } + }); + + test.beforeEach(async ({}, testInfo) => { + testLogger.testStart(testInfo.title, testInfo.file); + }); + + for (const width of WIDTHS) + for (const subTab of SUB_TABS) { + test( + `the title is fully visible and clear of the app picker on ${subTab} at ${width}px`, + { tag: TAGS }, + async ({ page }) => { + await page.setViewportSize({ width, height: 800 }); + const pa = new PageManager(page).rumProductAnalyticsPage; + await pa.goto(subTab, { app: seed.appId, period: "7d" }); + const title = page.locator('[data-test="rum-analytics-title"]'); + const picker = page + .locator( + '[data-test="rum-analytics-app-select"], [data-test="rum-analytics-app-label"]', + ) + .first(); + await expect(title).toBeVisible({ timeout: 30000 }); + await expect(picker).toBeVisible({ timeout: 30000 }); + await expect(title).toContainText(/Product analytics/i); + + const wide = width >= WIDE_FROM; + await expect + .poll( + () => + page.evaluate((isWide) => { + const q = (sel) => document.querySelector(sel); + const rect = (el) => el.getBoundingClientRect(); + const header = q(".app-page-header"); + const title = q('[data-test="rum-analytics-title"]'); + const picker = q( + '[data-test="rum-analytics-app-select"], [data-test="rum-analytics-app-label"]', + ); + const tabs = q('[data-test^="rum-analytics-subtab-"]'); + const host = q('[data-test="rum-analytics-filters"]'); + if (!header || !title || !picker || !tabs || !host) + return ["missing element"]; + const h = rect(header); + const t = rect(title); + const p = rect(picker); + const tabTop = rect(tabs).top; + const range = document.createRange(); + range.selectNodeContents(title); + const bad = []; + if (t.width < range.getBoundingClientRect().width) + bad.push("title truncated"); + if ( + t.left < p.right && + p.left < t.right && + t.top < p.bottom && + p.top < t.bottom + ) + bad.push("title overlaps picker"); + const inHeader = header.contains(host); + if (isWide) { + if (!inHeader) bad.push("filters not in header"); + if (getComputedStyle(host).display !== "contents") + bad.push("filters wrapper is not display: contents"); + if (p.bottom > h.bottom + 1 || p.top < h.top - 1) + bad.push("picker outside header"); + } else { + if (inHeader) bad.push("filters still in header"); + const hb = rect(host); + if (hb.top < h.bottom - 1) + bad.push("band above header bottom"); + if (hb.bottom > tabTop + 1) bad.push("band covers tabs"); + for (const c of host.children) { + if (rect(c).bottom > tabTop + 1) + bad.push("filter covers tabs"); + } + } + for (const sel of [ + '[data-test="rum-analytics-date-picker"]', + '[data-test="rum-analytics-refresh-btn"]', + '[data-test="rum-analytics-share-btn"]', + ]) { + const el = q(sel); + if (!el) continue; + const r = rect(el); + if (r.bottom > h.bottom + 1 || r.top < h.top - 1) + bad.push(`${sel} outside header`); + } + if (t.bottom > h.bottom + 1) bad.push("title outside header"); + return bad; + }, wide), + { timeout: 15000 }, + ) + .toEqual([]); + }, + ); + } +}); diff --git a/tests/ui-testing/playwright-tests/RegressionSet/RUM/rum-3268-heatmap-no-view-id.spec.js b/tests/ui-testing/playwright-tests/RegressionSet/RUM/rum-3268-heatmap-no-view-id.spec.js new file mode 100644 index 0000000000..ef45a60734 --- /dev/null +++ b/tests/ui-testing/playwright-tests/RegressionSet/RUM/rum-3268-heatmap-no-view-id.spec.js @@ -0,0 +1,92 @@ +// Heatmaps on an org whose _sessionreplay stream has no view_id (older SDK replay) must show the older-SDK state, not a 400. + +const { test, expect } = require("../../utils/enhanced-baseFixtures.js"); +const testLogger = require("../../utils/test-logger.js"); +const PageManager = require("../../../pages/page-manager.js"); +const { sqlOf } = require("../../../pages/rumPages/rumProductAnalyticsPage.js"); +const { + ensureRumStateOrg, + seedBuilt, + ensureSessionReplayStream, + buildFunnelSessions, + runAppId, +} = require("../../utils/rum-analytics-ingestion.js"); + +const TAGS = [ + "@rum", + "@rumAnalytics", + "@bug-3268", + "@P3", + "@all", + "@regression", + "@rumRegression", +]; +const SEARCH_URL = /\/api\/[^/]+\/_search(_stream)?(\?|$)/; +const PAGE_URL = "https://shop.example.com/web/a"; + +test.describe("RUM Heatmaps without view_id in _sessionreplay", () => { + test.describe.configure({ mode: "default" }); + test.use({ timezoneId: "UTC" }); + + let org = null; + let appId = null; + + test.beforeAll(async ({ browser }) => { + const page = await browser.newPage(); + try { + const { identifier } = await ensureRumStateOrg(page, "pa_hmnovid"); + const nowMs = Date.now(); + const seed = buildFunnelSessions(runAppId("pa-hmnovid"), nowMs, { + replay: false, + }); + const facts = await seedBuilt(page, seed, { nowMs, orgId: identifier }); + // The marker row carries no view_id, so the stream schema lacks the field. + await ensureSessionReplayStream(page, facts.appId, nowMs, { + orgId: identifier, + }); + org = identifier; + appId = facts.appId; + } finally { + await page.close(); + } + }); + + test.beforeEach(async ({}, testInfo) => { + testLogger.testStart(testInfo.title, testInfo.file); + }); + + test( + "the page snapshot shows the no-replay state and sends no view_id query", + { + tag: TAGS, + }, + async ({ page }) => { + const pa = new PageManager(page).rumProductAnalyticsPage; + const bad = []; + const replaySql = []; + page.on("response", (r) => { + if (!SEARCH_URL.test(r.url())) return; + const sql = sqlOf(r.request()); + if (/FROM "_sessionreplay"/.test(sql)) replaySql.push(sql); + if (r.status() === 400) bad.push(sql.slice(0, 300)); + }); + + await pa.goto( + "heatmaps", + { app: appId, period: "7d", page_url: PAGE_URL }, + { org }, + ); + + const notice = page.locator('[data-test="rum-heatmap-snapshot-error"]'); + await expect(notice).toBeVisible({ timeout: 60000 }); + await expect(notice).toContainText("Page snapshots need a newer RUM SDK"); + await expect(notice).not.toContainText("Couldn't load the page snapshot"); + await pa.waitForNetworkQuiet(); + expect( + replaySql.filter((s) => /view_id/.test(s)), + "no _sessionreplay query references view_id", + ).toEqual([]); + expect(bad, bad.join("\n")).toEqual([]); + }, + ); +}); diff --git a/tests/ui-testing/playwright-tests/RegressionSet/RUM/rum-3268-no-widen-at-30d.spec.js b/tests/ui-testing/playwright-tests/RegressionSet/RUM/rum-3268-no-widen-at-30d.spec.js new file mode 100644 index 0000000000..fc99472f47 --- /dev/null +++ b/tests/ui-testing/playwright-tests/RegressionSet/RUM/rum-3268-no-widen-at-30d.spec.js @@ -0,0 +1,52 @@ +// Product Analytics empty state offers "Show the past 30 days" only when the range is shorter than 30 days. + +const { test, expect } = require("../../utils/enhanced-baseFixtures.js"); +const testLogger = require("../../utils/test-logger.js"); +const PageManager = require("../../../pages/page-manager.js"); +const { + runAppId, + seedViewsOnlyApp, +} = require("../../utils/rum-analytics-ingestion.js"); + +const TAGS = ["@bug-3268", "@P3", "@regression", "@rumRegression"]; + +test.describe("Product Analytics widen-range action", () => { + test.use({ timezoneId: "UTC" }); + const emptyAppId = runAppId("pa-3268-empty"); + + // Another app's rows give the org a _rumdata schema, so the empty app shows the empty overview, not onboarding. + test.beforeAll(async ({ browser }) => { + const page = await browser.newPage(); + try { + await seedViewsOnlyApp(page, { appId: runAppId("pa-3268-other") }); + } finally { + await page.close(); + } + }); + + test.beforeEach(async ({}, testInfo) => { + testLogger.testStart(testInfo.title, testInfo.file); + }); + + test( + "the empty overview has no widen button at the past 30 days", + { tag: TAGS }, + async ({ page }) => { + const pa = new PageManager(page).rumProductAnalyticsPage; + await pa.goto("overview", { app: emptyAppId, period: "30d" }); + await expect(pa.overviewEmpty).toBeVisible({ timeout: 30000 }); + await expect(pa.widenRangeBtn).toHaveCount(0); + }, + ); + + test( + "the empty overview still offers the widen button at the past 1 day", + { tag: TAGS }, + async ({ page }) => { + const pa = new PageManager(page).rumProductAnalyticsPage; + await pa.goto("overview", { app: emptyAppId, period: "1d" }); + await expect(pa.overviewEmpty).toBeVisible({ timeout: 30000 }); + await expect(pa.widenRangeBtn).toBeVisible(); + }, + ); +}); diff --git a/tests/ui-testing/playwright-tests/RegressionSet/RUM/rum-3268-session-link-no-nan.spec.js b/tests/ui-testing/playwright-tests/RegressionSet/RUM/rum-3268-session-link-no-nan.spec.js new file mode 100644 index 0000000000..97bb6d6b4c --- /dev/null +++ b/tests/ui-testing/playwright-tests/RegressionSet/RUM/rum-3268-session-link-no-nan.spec.js @@ -0,0 +1,99 @@ +const { + test, + expect, + navigateToBase, +} = require("../../utils/enhanced-baseFixtures.js"); +const testLogger = require("../../utils/test-logger.js"); +const PageManager = require("../../../pages/page-manager.js"); +const { + SessionBuilder, + runAppId, + seedBuilt, +} = require("../../utils/rum-analytics-ingestion.js"); + +const TAGS = ["@bug-3268", "@P2", "@regression", "@rumRegression"]; +const HEATMAP_BTN = '[data-test="rum-app-sessions-view-heatmap-btn"]'; +const DURATION = '[data-test="session-viewer-duration"]'; + +test.describe("RUM Sessions list links carry a real time range", () => { + test.describe.configure({ mode: "default" }); + const appId = runAppId("pa-3268-nan"); + + test.beforeAll(async ({ browser }) => { + const page = await browser.newPage(); + try { + const nowMs = Date.now(); + const session = new SessionBuilder( + appId, + `${appId}-s1`, + nowMs - 20 * 60 * 1000, + { user: "nan@e2e.test" }, + ); + session + .view("https://shop.example.com/web/a") + .wait(60000) + .click("buy", "https://shop.example.com/web/a"); + session.rows.forEach((row) => { + row.service = appId; + }); + await seedBuilt( + page, + { rows: session.rows, facts: { appId } }, + { nowMs }, + ); + } finally { + await page.close(); + } + }); + + test.beforeEach(async ({ page }, testInfo) => { + testLogger.testStart(testInfo.title, testInfo.file); + await navigateToBase(page); + }); + + test( + "opening a session without replay puts finite start_time and end_time in the URL and keeps its duration on reload", + { + tag: TAGS, + }, + async ({ page }) => { + const pm = new PageManager(page); + const sessions = pm.rumSessionsPage; + + await sessions.gotoSessionsList({ service: appId, period: "1d" }); + await sessions.expectSessionsTableVisible(); + await sessions.waitForSessionRowsPresent(); + + const noReplayRow = sessions.tableRow + .filter({ hasNot: page.locator(HEATMAP_BTN) }) + .first(); + await expect(noReplayRow).toBeVisible(); + + await noReplayRow.click(); + await expect(page).toHaveURL(/\/rum\/sessions\/view\//, { + timeout: 15000, + }); + + const url = page.url(); + testLogger.info("Session viewer URL", { url }); + expect(url).not.toContain("NaN"); + const query = new URL(url).searchParams; + for (const key of ["start_time", "end_time"]) { + expect( + Number.isFinite(Number(query.get(key))), + `${key} must be a finite number`, + ).toBe(true); + expect( + Number(query.get(key)), + `${key} must be a real timestamp`, + ).toBeGreaterThan(0); + } + expect(Number(query.get("end_time"))).toBeGreaterThanOrEqual( + Number(query.get("start_time")), + ); + + await page.reload(); + await expect(page.locator(DURATION)).toBeVisible({ timeout: 30000 }); + }, + ); +}); diff --git a/tests/ui-testing/playwright-tests/RegressionSet/RUM/rum-3268-trace-duration-skew.spec.js b/tests/ui-testing/playwright-tests/RegressionSet/RUM/rum-3268-trace-duration-skew.spec.js new file mode 100644 index 0000000000..43a183612a --- /dev/null +++ b/tests/ui-testing/playwright-tests/RegressionSet/RUM/rum-3268-trace-duration-skew.spec.js @@ -0,0 +1,217 @@ +// Clock skew made the trace end precede the event date, so the duration clamped to 0us; fall back to the span length. + +const { + test, + expect, + navigateToBase, +} = require("../../utils/enhanced-baseFixtures.js"); +const testLogger = require("../../utils/test-logger.js"); +const { + getAuthHeaders, + getOrgIdentifier, +} = require("../../utils/cloud-auth.js"); +const crypto = require("crypto"); +const { ingestBase } = require("../../utils/ingest-url.js"); + +const TRACE_ID_FIELD = "_o2_trace_id"; +const SKEW_MS = 5000; +const SPAN_MS = 150; + +const hex = (bytes) => crypto.randomBytes(bytes).toString("hex"); + +function buildFixture() { + return { + sessionId: `e2e${hex(6)}`, + viewId: hex(8), + traceId: hex(16), + now: Date.now(), + op: "GET /api/skewed", + url: "https://shop.example.com/api/skewed", + }; +} + +function rumRows({ sessionId, viewId, traceId, now, url }) { + const base = { + session_id: sessionId, + view_id: viewId, + view_url: "https://shop.example.com/checkout", + view_loading_type: "initial_load", + service: "shop-frontend", + application_id: "e2e-rum-app", + source: "browser", + }; + const traceEnd = now - 60000 + SPAN_MS; + return [ + { + ...base, + _timestamp: (now - 60000) * 1000, + date: now - 60000, + type: "view", + usr_id: "e2e-user", + }, + { + ...base, + _timestamp: (now - 60000 + 500) * 1000, + date: traceEnd + SKEW_MS, + type: "resource", + resource_url: url, + resource_type: "fetch", + resource_method: "GET", + resource_status_code: 200, + resource_duration: SPAN_MS * 1000000, + [TRACE_ID_FIELD]: traceId, + }, + ]; +} + +function tracePayload({ now, traceId, op, url }) { + const attr = (key, value) => ({ key, value: { stringValue: String(value) } }); + const startNs = BigInt(now - 60000) * 1000000n; + return { + resourceSpans: [ + { + resource: { attributes: [attr("service.name", "shop-api")] }, + scopeSpans: [ + { + scope: { name: "rum-skew" }, + spans: [ + { + traceId, + spanId: hex(8), + name: op, + kind: 2, + startTimeUnixNano: String(startNs), + endTimeUnixNano: String(startNs + BigInt(SPAN_MS) * 1000000n), + attributes: [attr("http.url", url)], + status: { code: 1 }, + }, + ], + }, + ], + }, + ], + }; +} + +test.describe("RUM session trace duration under clock skew", () => { + test.describe.configure({ mode: "serial" }); + + let fixture; + + test.beforeAll(async ({ browser }) => { + const context = await browser.newContext({ + storageState: "playwright-tests/utils/auth/user.json", + }); + const page = await context.newPage(); + try { + fixture = buildFixture(); + const org = getOrgIdentifier() || "default"; + const headers = getAuthHeaders(); + const base = ( + process.env["INGESTION_URL"] || + process.env["ZO_BASE_URL"] || + "" + ).replace(/\/+$/, ""); + + const rum = await page.request.post( + `${ingestBase(base)}/api/${org}/_rumdata/_json`, + { + headers, + data: rumRows(fixture), + }, + ); + expect(rum.status(), "RUM events must ingest").toBe(200); + + const traces = await page.request.post( + `${ingestBase(base)}/api/${org}/v1/traces`, + { + headers, + data: tracePayload(fixture), + }, + ); + expect(traces.status(), "skewed trace must ingest").toBe(200); + + await expect + .poll( + async () => { + const res = await page.request.post( + `${base}/api/${org}/_search?type=logs`, + { + headers, + data: { + query: { + sql: `SELECT COUNT(*) AS n FROM "_rumdata" WHERE session_id = '${fixture.sessionId}'`, + start_time: (fixture.now - 3600000) * 1000, + end_time: (fixture.now + 3600000) * 1000, + from: 0, + size: 1, + }, + }, + }, + ); + if (!res.ok()) return 0; + return Number(((await res.json()).hits || [])[0]?.n || 0); + }, + { message: "RUM events must be searchable", timeout: 90000 }, + ) + .toBeGreaterThanOrEqual(2); + + testLogger.info("Ingested RUM skew fixture", { + sessionId: fixture.sessionId, + }); + } finally { + await page.close(); + await context.close(); + } + }); + + test.beforeEach(async ({ page }, testInfo) => { + testLogger.testStart(testInfo.title, testInfo.file); + await navigateToBase(page); + const baseUrl = (process.env["ZO_BASE_URL"] || "").replace(/\/+$/, ""); + const org = getOrgIdentifier() || "default"; + await page.goto( + `${baseUrl}/web/rum/sessions/view/${fixture.sessionId}?org_identifier=${org}`, + ); + await page + .waitForLoadState("networkidle", { timeout: 30000 }) + .catch(() => {}); + }); + + test.afterEach(async ({}, testInfo) => { + testLogger.testEnd(testInfo.title, testInfo.status); + }); + + test( + "P3: a trace that ends before its RUM request shows the span duration, not 0us", + { + tag: [ + "@rum", + "@sessionReplay", + "@bug-3268", + "@regression", + "@P3", + "@all", + ], + }, + async ({ page }) => { + const row = page + .locator('[data-test^="o2-table-row-"]') + .filter({ hasText: fixture.op }); + // The trace lands in search after the RUM rows, so reload until it shows up. + await expect(async () => { + await page.reload(); + await page.locator('[data-test="tab-traces"]').click(); + await expect( + page.locator('[data-test="rum-player-traces-tab-table"]'), + ).toBeVisible({ timeout: 15000 }); + await expect(row).toHaveCount(1, { timeout: 5000 }); + }).toPass({ timeout: 90000, intervals: [3000] }); + // The duration is rendered from microsecond timestamps, so it may land a millisecond under the seeded span. + await expect(row).toContainText( + new RegExp(`(${SPAN_MS - 1}|${SPAN_MS})(\\.\\d+)?ms`), + ); + await expect(row).not.toContainText("0us"); + }, + ); +}); diff --git a/web/src/components/rum/PlayerTracesTab.spec.ts b/web/src/components/rum/PlayerTracesTab.spec.ts index db8f9b3891..c6e5c64345 100644 --- a/web/src/components/rum/PlayerTracesTab.spec.ts +++ b/web/src/components/rum/PlayerTracesTab.spec.ts @@ -883,6 +883,68 @@ describe("PlayerTracesTab", () => { expect(wrapper.text()).toContain("523ms"); }); + it("should fall back to the span duration when the trace ends before the RUM request", async () => { + wrapper.unmount(); + + // _date = 5000ms but the trace ended at 1150ms (clock skew); span lasted 150ms. + setupSuccessfulMocks( + [createRumHit({ _date: 5000 })], + [createTraceMetadata({ start_time: 1_000_000_000, end_time: 1_150_000_000 })], + ); + + wrapper = mountComponent(); + await flushPromises(); + + expect(wrapper.text()).toContain("150ms"); + expect(wrapper.text()).not.toContain("0us"); + + await wrapper.find('[data-test="table-row-0"]').trigger("click"); + await nextTick(); + expect(wrapper.text()).toContain("150ms"); + }); + + it("should show a dash and no timer chip when the trace has no usable duration", async () => { + wrapper.unmount(); + + // Trace ended before the RUM request and the span has zero length. + setupSuccessfulMocks( + [createRumHit({ _date: 5000 })], + [createTraceMetadata({ start_time: 1_000_000_000, end_time: 1_000_000_000 })], + ); + + wrapper = mountComponent(); + await flushPromises(); + + expect(wrapper.find('[data-test="table-row-0"]').text()).toContain("—"); + + await wrapper.find('[data-test="table-row-0"]').trigger("click"); + await nextTick(); + expect(wrapper.find('[data-test="rum-player-traces-tab-seek-btn"]').exists()).toBe(true); + expect(wrapper.html()).not.toContain('name="timer"'); + }); + + it("should match the Slow filter on the span duration when the trace ends before the RUM request", async () => { + wrapper.unmount(); + + setupSuccessfulMocks( + [createRumHit({ _date: 9000 })], + [createTraceMetadata({ start_time: 1_000_000_000, end_time: 2_500_000_000 })], + ); + + wrapper = mountComponent(); + await flushPromises(); + + const status = wrapper + .findAllComponents({ name: "OSelect" }) + .find( + (select) => select.attributes("data-test") === "rum-player-traces-tab-status-filter", + )!; + status.vm.$emit("update:modelValue", ["slow"]); + await flushPromises(); + + expect(wrapper.findAll('[data-test^="table-row-"]')).toHaveLength(1); + }); + it("should display error count badge in detail header when trace has errors", async () => { wrapper.unmount(); diff --git a/web/src/components/rum/PlayerTracesTab.vue b/web/src/components/rum/PlayerTracesTab.vue index b595ceb74e..8c206380a7 100644 --- a/web/src/components/rum/PlayerTracesTab.vue +++ b/web/src/components/rum/PlayerTracesTab.vue @@ -94,7 +94,7 @@ along with this program. If not, see . {{ traceTimeOffset(selectedTrace.metadata.start_time) }} @@ -231,7 +231,11 @@ along with this program. If not, see .