Browse Source

feat(replays): added decompression for replay attachments (#35365)

Closes #35150 
Installed pako so we can decompress rrweb attachments for replays.
Dublin Anondson 2 years ago
parent
commit
09e53527e9

+ 1 - 0
package.json

@@ -102,6 +102,7 @@
     "mockdate": "3.0.5",
     "moment": "2.29.3",
     "moment-timezone": "0.5.34",
+    "pako": "^2.0.4",
     "papaparse": "^5.3.2",
     "pegjs": "^0.10.0",
     "pegjs-loader": "^0.5.6",

+ 43 - 2
static/app/utils/replays/hooks/useReplayData.tsx

@@ -1,5 +1,6 @@
 import {useCallback, useEffect, useMemo, useState} from 'react';
 import * as Sentry from '@sentry/react';
+import {inflate} from 'pako';
 
 import {IssueAttachment} from 'sentry/types';
 import {EventTransaction} from 'sentry/types/event';
@@ -67,6 +68,25 @@ const IS_RRWEB_ATTACHMENT_FILENAME = /rrweb-[0-9]{13}.json/;
 function isRRWebEventAttachment(attachment: IssueAttachment) {
   return IS_RRWEB_ATTACHMENT_FILENAME.test(attachment.name);
 }
+export function mapRRWebAttachments(unsortedReplayAttachments): ReplayAttachment {
+  const replayAttachments: ReplayAttachment = {
+    breadcrumbs: [],
+    replaySpans: [],
+    recording: [],
+  };
+
+  unsortedReplayAttachments.forEach(attachment => {
+    if (attachment.data?.tag === 'performanceSpan') {
+      replayAttachments.replaySpans.push(attachment.data.payload);
+    } else if (attachment?.data?.tag === 'breadcrumb') {
+      replayAttachments.breadcrumbs.push(attachment.data.payload);
+    } else {
+      replayAttachments.recording.push(attachment);
+    }
+  });
+
+  return replayAttachments;
+}
 
 const INITIAL_STATE: State = Object.freeze({
   event: undefined,
@@ -115,9 +135,30 @@ function useReplayData({eventSlug, orgId}: Options): Result {
     const attachments = await Promise.all(
       rrwebAttachmentIds.map(async attachment => {
         const response = await api.requestPromise(
-          `/api/0/projects/${orgId}/${projectId}/events/${eventId}/attachments/${attachment.id}/?download`
+          `/api/0/projects/${orgId}/${projectId}/events/${eventId}/attachments/${attachment.id}/?download`,
+          {
+            includeAllArgs: true,
+          }
         );
-        return JSON.parse(response) as ReplayAttachment;
+
+        // for non-compressed events, parse and return
+        try {
+          return JSON.parse(response[0]) as ReplayAttachment;
+        } catch (error) {
+          // swallow exception.. if we can't parse it, it's going to be compressed
+        }
+
+        // for non-compressed events, parse and return
+        try {
+          // for compressed events, inflate the blob and map the events
+          const responseBlob = await response[2]?.rawResponse.blob();
+          const responseArray = (await responseBlob?.arrayBuffer()) as Uint8Array;
+          const parsedPayload = JSON.parse(inflate(responseArray, {to: 'string'}));
+          const replayAttachments = mapRRWebAttachments(parsedPayload);
+          return replayAttachments;
+        } catch (error) {
+          return {};
+        }
       })
     );
 

+ 94 - 0
tests/js/spec/utils/replays/useReplayData.test.jsx

@@ -0,0 +1,94 @@
+const {mapRRWebAttachments} = require('sentry/utils/replays/hooks/useReplayData');
+
+const testPayload = [
+  {
+    type: 3,
+    data: {
+      source: 1,
+      positions: [
+        {x: 737, y: 553, id: 46, timeOffset: -446},
+        {x: 655, y: 614, id: 52, timeOffset: -385},
+        {x: 653, y: 614, id: 52, timeOffset: -285},
+        {x: 653, y: 613, id: 52, timeOffset: -226},
+        {x: 653, y: 613, id: 52, timeOffset: -171},
+        {x: 662, y: 601, id: 50, timeOffset: -105},
+        {x: 671, y: 591, id: 50, timeOffset: -46},
+      ],
+    },
+    timestamp: 1654290037123,
+  },
+  {
+    type: 3,
+    data: {
+      source: 0,
+      texts: [],
+      attributes: [],
+      removes: [],
+      adds: [
+        {
+          parentId: 33,
+          nextId: null,
+          node: {
+            type: 2,
+            tagName: 'com-1password-button',
+            attributes: {},
+            childNodes: [],
+            id: 65,
+          },
+        },
+      ],
+    },
+    timestamp: 1654290037561,
+  },
+  {
+    type: 5,
+    timestamp: 1654290037.267,
+    data: {
+      tag: 'breadcrumb',
+      payload: {
+        timestamp: 1654290037.267,
+        type: 'default',
+        category: 'ui.click',
+        message: 'body > div#root > div.App > form',
+        data: {nodeId: 44},
+      },
+    },
+  },
+  {
+    type: 5,
+    timestamp: 1654290034.2623,
+    data: {
+      tag: 'performanceSpan',
+      payload: {
+        op: 'navigation.navigate',
+        description: 'http://localhost:3000/',
+        startTimestamp: 1654290034.2623,
+        endTimestamp: 1654290034.5808,
+        data: {size: 1150},
+      },
+    },
+  },
+  {
+    type: 5,
+    timestamp: 1654290034.2623,
+    data: {
+      tag: 'performanceSpan',
+      payload: {
+        op: 'navigation.navigate',
+        description: 'http://localhost:3000/',
+        startTimestamp: 1654290034.2623,
+        endTimestamp: 1654290034.5808,
+        data: {size: 1150},
+      },
+    },
+  },
+];
+
+describe('useReplayData Hooks', () => {
+  it('t', () => {
+    const results = mapRRWebAttachments(testPayload);
+    expect(results.breadcrumbs.length).toBe(1);
+    expect(results.recording.length).toBe(2);
+    expect(results.replaySpans.length).toBe(2);
+  });
+});

+ 5 - 0
yarn.lock

@@ -11808,6 +11808,11 @@ p-try@^2.0.0:
   resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
   integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
 
+pako@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/pako/-/pako-2.0.4.tgz#6cebc4bbb0b6c73b0d5b8d7e8476e2b2fbea576d"
+  integrity sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg==
+
 papaparse@^5.3.2:
   version "5.3.2"
   resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-5.3.2.tgz#d1abed498a0ee299f103130a6109720404fbd467"