diff --git a/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx
index 2ad92eb8a..ce9c4153f 100644
--- a/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx
+++ b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx
@@ -37,6 +37,7 @@ const IMG = 'img';
const MEDIA = 'media';
const OTHER = 'other';
const WS = 'websocket';
+const GRAPHQL = 'graphql';
const TYPE_TO_TAB = {
[ResourceType.XHR]: XHR,
@@ -47,9 +48,10 @@ const TYPE_TO_TAB = {
[ResourceType.MEDIA]: MEDIA,
[ResourceType.WS]: WS,
[ResourceType.OTHER]: OTHER,
+ [ResourceType.GRAPHQL]: GRAPHQL,
};
-const TAP_KEYS = [ALL, XHR, JS, CSS, IMG, MEDIA, OTHER, WS] as const;
+const TAP_KEYS = [ALL, XHR, JS, CSS, IMG, MEDIA, OTHER, WS, GRAPHQL] as const;
export const NETWORK_TABS = TAP_KEYS.map((tab) => ({
text: tab === 'xhr' ? 'Fetch/XHR' : tab,
key: tab,
diff --git a/frontend/app/player/web/MessageLoader.ts b/frontend/app/player/web/MessageLoader.ts
index 4ff59cbf6..e52c31f6f 100644
--- a/frontend/app/player/web/MessageLoader.ts
+++ b/frontend/app/player/web/MessageLoader.ts
@@ -42,6 +42,21 @@ export default class MessageLoader {
this.session = session
}
+ /**
+ * TODO: has to be moved out of messageLoader logic somehow
+ * */
+ spriteMapSvg: SVGElement | null = null;
+ potentialSpriteMap: Record
= {};
+ domParser: DOMParser | null = null;
+ createSpriteMap = () => {
+ if (!this.spriteMapSvg) {
+ this.domParser = new DOMParser();
+ this.spriteMapSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.spriteMapSvg.setAttribute("style", "display: none;");
+ this.spriteMapSvg.setAttribute("id", "reconstructed-sprite");
+ }
+ }
+
createNewParser(
shouldDecrypt = true,
onMessagesDone: (msgs: PlayerMsg[], file?: string) => void,
@@ -78,7 +93,22 @@ export default class MessageLoader {
let artificialStartTime = Infinity;
let startTimeSet = false;
- msgs.forEach((msg) => {
+ msgs.forEach((msg, i) => {
+ if (msg.tp === MType.SetNodeAttribute) {
+ if (msg.value.includes('_$OPENREPLAY_SPRITE$_')) {
+ this.createSpriteMap()
+ if (!this.domParser) {
+ return console.error('DOM parser is not initialized?');
+ }
+ handleSprites(
+ this.potentialSpriteMap,
+ this.domParser,
+ msg,
+ this.spriteMapSvg!,
+ i
+ );
+ }
+ }
if (msg.tp === MType.Redux || msg.tp === MType.ReduxDeprecated) {
if ('actionTime' in msg && msg.actionTime) {
msg.time = msg.actionTime - this.session.startedAt;
@@ -293,6 +323,10 @@ export default class MessageLoader {
await Promise.allSettled([restDomFilesPromise, restDevtoolsFilesPromise]);
this.messageManager.onFileReadSuccess();
+ // no sprites for mobile
+ if (this.spriteMapSvg && 'injectSpriteMap' in this.messageManager) {
+ this.messageManager.injectSpriteMap(this.spriteMapSvg);
+ }
};
loadEFSMobs = async () => {
@@ -420,5 +454,28 @@ function findBrokenNodes(nodes: any[]) {
return result;
}
+function handleSprites(potentialSpriteMap: Record, parser: DOMParser, msg: Record, spriteMapSvg: SVGElement, i: number) {
+ const [_, dataUrl] = msg.value.split('_$OPENREPLAY_SPRITE$_');
+ const potentialSprite = potentialSpriteMap[dataUrl];
+ if (potentialSprite) {
+ msg.value = potentialSprite;
+ } else {
+ const svgText = atob(dataUrl.split(",")[1]);
+ const svgDoc = parser.parseFromString(svgText, "image/svg+xml");
+ const originalSvg = svgDoc.querySelector("svg");
+ if (originalSvg) {
+ const symbol = document.createElementNS("http://www.w3.org/2000/svg", "symbol");
+ const symbolId = `symbol-${msg.id || 'ind-' + i}`; // Generate an ID if missing
+ symbol.setAttribute("id", symbolId);
+ symbol.setAttribute("viewBox", originalSvg.getAttribute("viewBox") || "0 0 24 24");
+ symbol.innerHTML = originalSvg.innerHTML;
+
+ spriteMapSvg.appendChild(symbol);
+ msg.value = `#${symbolId}`;
+ potentialSpriteMap[dataUrl] = `#${symbolId}`;
+ }
+ }
+}
+
// @ts-ignore
window.searchOrphans = (msgs) => findBrokenNodes(msgs.filter(m => [8,9,10,70].includes(m.tp)));
diff --git a/frontend/app/player/web/MessageManager.ts b/frontend/app/player/web/MessageManager.ts
index ba074a1cc..d09c3f241 100644
--- a/frontend/app/player/web/MessageManager.ts
+++ b/frontend/app/player/web/MessageManager.ts
@@ -8,7 +8,7 @@ import ListWalker from '../common/ListWalker';
import MouseMoveManager from './managers/MouseMoveManager';
import ActivityManager from './managers/ActivityManager';
-import TabClosingManager from "./managers/TabClosingManager";
+import TabClosingManager from './managers/TabClosingManager';
import { MouseThrashing, MType } from './messages';
import type { Message, MouseClick } from './messages';
@@ -52,7 +52,7 @@ export interface State extends ScreenState {
};
tabNames: {
[tabId: string]: string;
- }
+ };
domContentLoadedTime?: { time: number; value: number };
domBuildingTime?: number;
@@ -99,7 +99,7 @@ export default class MessageManager {
closedTabs: [],
sessionStart: 0,
tabNames: {},
-};
+ };
private clickManager: ListWalker = new ListWalker();
private mouseThrashingManager: ListWalker = new ListWalker();
@@ -128,7 +128,9 @@ export default class MessageManager {
this.mouseMoveManager = new MouseMoveManager(screen);
this.sessionStart = this.session.startedAt;
state.update({ sessionStart: this.sessionStart });
- this.activityManager = new ActivityManager(this.session.duration.milliseconds); // only if not-live
+ this.activityManager = new ActivityManager(
+ this.session.duration.milliseconds
+ ); // only if not-live
}
public getListsFullState = () => {
@@ -139,12 +141,18 @@ export default class MessageManager {
return Object.values(this.tabs)[0].getListsFullState();
};
+ public injectSpriteMap = (spriteEl: SVGElement) => {
+ Object.values(this.tabs).forEach((tab) => {
+ tab.injectSpriteMap(spriteEl)
+ })
+ };
+
public setSession = (session: SessionFilesInfo) => {
this.session = session;
this.sessionStart = this.session.startedAt;
this.state.update({ sessionStart: this.sessionStart });
Object.values(this.tabs).forEach((tab) => tab.setSession(session));
- }
+ };
public updateLists(lists: RawList) {
Object.keys(this.tabs).forEach((tab) => {
@@ -198,26 +206,26 @@ export default class MessageManager {
* Scan tab managers for last message ts
* */
public createTabCloseEvents = () => {
- const lastMsgArr: [string, number][] = []
+ const lastMsgArr: [string, number][] = [];
if (this.tabsAmount === 1) {
return this.tabCloseManager.append({
tabId: Object.keys(this.tabs)[0],
- time: this.session.durationMs - 100
- })
+ time: this.session.durationMs - 100,
+ });
}
for (const [tabId, tab] of Object.entries(this.tabs)) {
- const { lastMessageTs } = tab
+ const { lastMessageTs } = tab;
if (lastMessageTs && tabId) {
- lastMsgArr.push([tabId, lastMessageTs])
+ lastMsgArr.push([tabId, lastMessageTs]);
}
}
- lastMsgArr.sort((a, b) => a[1] - b[1])
+ lastMsgArr.sort((a, b) => a[1] - b[1]);
lastMsgArr.forEach(([tabId, lastMessageTs]) => {
- this.tabCloseManager.append({ tabId, time: lastMessageTs })
- })
- }
+ this.tabCloseManager.append({ tabId, time: lastMessageTs });
+ });
+ };
public startLoading = () => {
this.waitingForFiles = true;
@@ -238,15 +246,15 @@ export default class MessageManager {
// usually means waiting for messages from live session
if (Object.keys(this.tabs).length === 0) return;
this.activeTabManager.moveReady(t).then(async (tabId) => {
- const closeMessage = await this.tabCloseManager.moveReady(t)
+ const closeMessage = await this.tabCloseManager.moveReady(t);
if (closeMessage) {
- const closedTabs = this.tabCloseManager.closedTabs
+ const closedTabs = this.tabCloseManager.closedTabs;
if (closedTabs.size === this.tabsAmount) {
if (this.session.durationMs - t < 250) {
- this.state.update({ closedTabs: Array.from(closedTabs) })
+ this.state.update({ closedTabs: Array.from(closedTabs) });
}
} else {
- this.state.update({ closedTabs: Array.from(closedTabs) })
+ this.state.update({ closedTabs: Array.from(closedTabs) });
}
}
// Moving mouse and setting :hover classes on ready view
@@ -261,7 +269,8 @@ export default class MessageManager {
this.screen.cursor.shake();
}
if (!this.activeTab) {
- this.activeTab = this.state.get().currentTab ?? Object.keys(this.tabs)[0];
+ this.activeTab =
+ this.state.get().currentTab ?? Object.keys(this.tabs)[0];
}
if (tabId) {
@@ -291,8 +300,7 @@ export default class MessageManager {
});
if (
this.waitingForFiles ||
- (this.lastMessageTime <= t &&
- t < this.session.durationMs)
+ (this.lastMessageTime <= t && t < this.session.durationMs)
) {
this.setMessagesLoading(true);
}
@@ -318,7 +326,12 @@ export default class MessageManager {
if (msg.tp === 9999) return;
if (!this.tabs[msg.tabId]) {
this.tabsAmount++;
- this.state.update({ tabStates: { ...this.state.get().tabStates, [msg.tabId]: TabSessionManager.INITIAL_STATE } });
+ this.state.update({
+ tabStates: {
+ ...this.state.get().tabStates,
+ [msg.tabId]: TabSessionManager.INITIAL_STATE,
+ },
+ });
this.tabs[msg.tabId] = new TabSessionManager(
this.session,
this.state,
@@ -368,7 +381,11 @@ export default class MessageManager {
switch (msg.tp) {
case MType.CreateDocument:
if (!this.firstVisualEventSet) {
- this.activeTabManager.unshift({ tp: MType.TabChange, tabId: msg.tabId, time: 0 });
+ this.activeTabManager.unshift({
+ tp: MType.TabChange,
+ tabId: msg.tabId,
+ time: 0,
+ });
this.state.update({
firstVisualEvent: msg.time,
currentTab: msg.tabId,
@@ -387,9 +404,11 @@ export default class MessageManager {
this.updateChangeEvents();
}
this.screen.display(!messagesLoading);
- const cssLoading = Object.values(this.state.get().tabStates).some((tab) => tab.cssLoading);
- const isReady = !messagesLoading && !cssLoading
- this.state.update({ messagesLoading, ready: isReady});
+ const cssLoading = Object.values(this.state.get().tabStates).some(
+ (tab) => tab.cssLoading
+ );
+ const isReady = !messagesLoading && !cssLoading;
+ this.state.update({ messagesLoading, ready: isReady });
};
decodeMessage(msg: Message) {
diff --git a/frontend/app/player/web/Screen/Screen.ts b/frontend/app/player/web/Screen/Screen.ts
index d643206f4..8399ab867 100644
--- a/frontend/app/player/web/Screen/Screen.ts
+++ b/frontend/app/player/web/Screen/Screen.ts
@@ -181,7 +181,7 @@ export default class Screen {
getElementFromInternalPoint({ x, y }: Point): Element | null {
// elementFromPoint && elementFromPoints require viewpoint-related coordinates,
- // not document-related
+ // not document-related
return this.document?.elementFromPoint(x, y) || null;
}
diff --git a/frontend/app/player/web/TabManager.ts b/frontend/app/player/web/TabManager.ts
index 6785678b3..4e2cb61d3 100644
--- a/frontend/app/player/web/TabManager.ts
+++ b/frontend/app/player/web/TabManager.ts
@@ -121,6 +121,10 @@ export default class TabSessionManager {
return this.pagesManager.getNode(id);
};
+ public injectSpriteMap = (spriteMapEl: SVGElement) => {
+ this.pagesManager.injectSpriteMap(spriteMapEl);
+ }
+
public updateLists(lists: Partial) {
Object.keys(lists).forEach((key: 'event' | 'stack' | 'exceptions') => {
const currentList = this.lists.lists[key];
diff --git a/frontend/app/player/web/managers/DOM/DOMManager.ts b/frontend/app/player/web/managers/DOM/DOMManager.ts
index 3003f0b79..a89831c57 100644
--- a/frontend/app/player/web/managers/DOM/DOMManager.ts
+++ b/frontend/app/player/web/managers/DOM/DOMManager.ts
@@ -7,7 +7,7 @@ import ListWalker from '../../../common/ListWalker';
import StylesManager from './StylesManager';
import FocusManager from './FocusManager';
import SelectionManager from './SelectionManager';
-import type { StyleElement } from './VirtualDOM';
+import { StyleElement, VSpriteMap } from "./VirtualDOM";
import {
OnloadStyleSheet,
VDocument,
@@ -157,6 +157,12 @@ export default class DOMManager extends ListWalker {
return;
}
const parent = this.vElements.get(parentID) || this.olVRoots.get(parentID);
+ if ('tagName' in child && child.tagName === 'BODY') {
+ const spriteMap = new VSpriteMap('svg', true, Number.MAX_SAFE_INTEGER - 100, Number.MAX_SAFE_INTEGER - 100);
+ spriteMap.node.setAttribute('id', 'OPENREPLAY_SPRITES_MAP');
+ spriteMap.node.setAttribute('style', 'display: none;');
+ child.insertChildAt(spriteMap, Number.MAX_SAFE_INTEGER - 100);
+ }
if (!parent) {
logger.error(
`${id} Insert error. Parent vNode ${parentID} not found`,
diff --git a/frontend/app/player/web/managers/DOM/VirtualDOM.ts b/frontend/app/player/web/managers/DOM/VirtualDOM.ts
index be3516f79..0677af117 100644
--- a/frontend/app/player/web/managers/DOM/VirtualDOM.ts
+++ b/frontend/app/player/web/managers/DOM/VirtualDOM.ts
@@ -53,7 +53,7 @@ export abstract class VNode {
public abstract applyChanges(): void
}
-type VChild = VElement | VText
+type VChild = VElement | VText | VSpriteMap
abstract class VParent extends VNode{
/**
*/
@@ -140,6 +140,44 @@ export class VShadowRoot extends VParent {
export type VRoot = VDocument | VShadowRoot
+export class VSpriteMap extends VParent {
+ parentNode: VParent | null =
+ null; /** Should be modified only by he parent itself */
+ private newAttributes: Map = new Map();
+
+ constructor(
+ readonly tagName: string,
+ readonly isSVG = true,
+ public readonly index: number,
+ private readonly nodeId: number
+ ) {
+ super();
+ this.createNode();
+ }
+ protected createNode() {
+ try {
+ const element = document.createElementNS(
+ 'http://www.w3.org/2000/svg',
+ this.tagName
+ );
+ element.dataset['openreplayId'] = this.nodeId.toString();
+ return element;
+ } catch (e) {
+ console.error(
+ 'Openreplay: Player received invalid html tag',
+ this.tagName,
+ e
+ );
+ return document.createElement(this.tagName.replace(/[^a-z]/gi, ''));
+ }
+ }
+
+ applyChanges() {
+ // this is a hack to prevent the sprite map from being removed from the DOM
+ return null;
+ }
+}
+
export class VElement extends VParent {
parentNode: VParent | null = null /** Should be modified only by he parent itself */
private newAttributes: Map = new Map()
diff --git a/frontend/app/player/web/managers/PagesManager.ts b/frontend/app/player/web/managers/PagesManager.ts
index 4ec537b67..8e5650d71 100644
--- a/frontend/app/player/web/managers/PagesManager.ts
+++ b/frontend/app/player/web/managers/PagesManager.ts
@@ -79,15 +79,41 @@ export default class PagesManager extends ListWalker {
return this.currentPage?.getNode(id);
}
+ spriteMapEl: SVGElement | null = null;
+ injectSpriteMap = (spriteEl: SVGElement) => {
+ this.spriteMapEl = spriteEl;
+ this.refreshSprites();
+ };
+
+ refreshSprites = () => {
+ const int = setInterval(() => {
+ const potential = this.screen.document?.body.querySelector(
+ '#OPENREPLAY_SPRITES_MAP'
+ );
+ if (potential) {
+ potential.innerHTML = this.spriteMapEl!.innerHTML;
+ clearInterval(int);
+ }
+ }, 250);
+ }
+
moveReady(t: number): Promise {
const requiredPage = this.moveGetLast(t);
+ let changed = false;
if (requiredPage != null) {
this.currentPage?.clearSelectionManager();
this.currentPage = requiredPage;
this.currentPage.reset(); // Otherwise it won't apply create_document
+ changed = true;
}
if (this.currentPage != null) {
- return this.currentPage.moveReady(t);
+ return this.currentPage.moveReady(t).then(() => {
+ if (changed && this.spriteMapEl) {
+ setTimeout(() => {
+ this.refreshSprites();
+ }, 0)
+ }
+ })
}
return Promise.resolve();
}
diff --git a/frontend/app/player/web/types/resource.ts b/frontend/app/player/web/types/resource.ts
index 86873a3bb..53944ab4a 100644
--- a/frontend/app/player/web/types/resource.ts
+++ b/frontend/app/player/web/types/resource.ts
@@ -105,13 +105,26 @@ export interface IResourceRequest extends IResource {
decodedBodySize?: number,
}
+const getGraphqlReqName = (resource: IResource) => {
+ try {
+ if (!resource.request) return getResourceName(resource.url)
+ const req = JSON.parse(resource.request)
+ const body = JSON.parse(req.body)
+ return /query (\w+)/.exec(body.query)?.[1]
+ } catch (e) {
+ return getResourceName(resource.url)
+ }
+}
-export const Resource = (resource: IResource) => ({
- ...resource,
- name: getResourceName(resource.url),
- isRed: !resource.success || resource.error, //|| resource.score >= RED_BOUND,
- isYellow: false, // resource.score < RED_BOUND && resource.score >= YELLOW_BOUND,
-})
+export const Resource = (resource: IResource) => {
+ const name = resource.type === 'graphql' ? getGraphqlReqName(resource) : getResourceName(resource.url)
+ return {
+ ...resource,
+ name,
+ isRed: !resource.success || resource.error, //|| resource.score >= RED_BOUND,
+ isYellow: false, // resource.score < RED_BOUND && resource.score >= YELLOW_BOUND,
+ }
+}
export function getResourceFromResourceTiming(msg: ResourceTiming, sessStart: number) {
diff --git a/tracker/tracker-graphql/README.md b/tracker/tracker-graphql/README.md
index 9c68794c5..07a0b8678 100644
--- a/tracker/tracker-graphql/README.md
+++ b/tracker/tracker-graphql/README.md
@@ -30,16 +30,50 @@ export const recordGraphQL = tracker.use(createGraphqlMiddleware());
### Relay
If you're using [Relay network tools](https://github.com/relay-tools/react-relay-network-modern),
-you can simply [create a middleware](https://github.com/relay-tools/react-relay-network-modern/tree/master?tab=readme-ov-file#example-of-injecting-networklayer-with-middlewares-on-the-client-side)
+you can simply [create a middleware](https://github.com/relay-tools/react-relay-network-modern/tree/master?tab=readme-ov-file#example-of-injecting-networklayer-with-middlewares-on-the-client-side) (async based); otherwise this will require wrapping fetch function with Observable.
```js
import { createRelayMiddleware } from '@openreplay/tracker-graphql';
+import { Observable } from 'relay-runtime';
-const trackerMiddleware = tracker.use(createRelayMiddleware());
+const withTracker = tracker.use(createRelayMiddleware())
+function createFetchObservable(operation, variables) {
+ return Observable.create(sink => {
+ fetch(`YOUR URL`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ query: operation.text, variables }),
+ })
+ .then(response => {
+ if (!response.ok) {
+ throw new Error(response.statusText);
+ }
+ return response.json();
+ })
+ .then(data => {
+ sink.next(data);
+ sink.complete();
+ })
+ .catch(error => {
+ sink.error(error);
+ })
+ });
+}
+
+const network = Network.create(withTracker(createFetchObservable));
+
+const environment = new Environment({
+ network,
+ store: new Store(new RecordSource()),
+});
+```
+
+```js
+import { createRelayToolsMiddleware } from '@openreplay/tracker-graphql';
+
+const trackerMiddleware = tracker.use(createRelayToolsMiddleware());
const network = new RelayNetworkLayer([
- // your middleware
- // ,
trackerMiddleware,
]);
```
diff --git a/tracker/tracker-graphql/package.json b/tracker/tracker-graphql/package.json
index e8c7d9981..cb5c8dd2e 100644
--- a/tracker/tracker-graphql/package.json
+++ b/tracker/tracker-graphql/package.json
@@ -1,7 +1,7 @@
{
"name": "@openreplay/tracker-graphql",
"description": "Tracker plugin for GraphQL requests recording",
- "version": "4.1.0",
+ "version": "4.2.0",
"keywords": [
"graphql",
"logging",
@@ -31,7 +31,6 @@
"typescript": "^5.3.3"
},
"dependencies": {
- "@apollo/client": "^3.9.5",
"@types/zen-observable": "^0.8.7",
"zen-observable": "^0.10.0"
}
diff --git a/tracker/tracker-graphql/src/index.ts b/tracker/tracker-graphql/src/index.ts
index 5bf81f24c..d09fba2ac 100644
--- a/tracker/tracker-graphql/src/index.ts
+++ b/tracker/tracker-graphql/src/index.ts
@@ -1,10 +1,11 @@
import createTrackerLink from './apolloMiddleware.js';
-import createRelayMiddleware from './relayMiddleware.js';
+import { createRelayObserver, createRelayMiddleware } from './relayMiddleware.js';
import createGraphqlMiddleware from './graphqlMiddleware.js';
import { Sanitizer } from './types.js';
export {
createTrackerLink,
+ createRelayObserver,
createRelayMiddleware,
createGraphqlMiddleware,
Sanitizer,
diff --git a/tracker/tracker-graphql/src/relayMiddleware.ts b/tracker/tracker-graphql/src/relayMiddleware.ts
index c3aac9d98..6e02c6545 100644
--- a/tracker/tracker-graphql/src/relayMiddleware.ts
+++ b/tracker/tracker-graphql/src/relayMiddleware.ts
@@ -1,6 +1,95 @@
import { App, Messages } from '@openreplay/tracker';
import type { Middleware, RelayRequest } from './relaytypes';
import { Sanitizer } from './types';
+import Observable from 'zen-observable';
+
+interface GraphQLOperation {
+ name: string;
+ operationKind: string;
+ text?: string | null;
+}
+
+interface GraphQLVariables {
+ [key: string]: any;
+}
+
+interface GraphQLCacheConfig {
+ [key: string]: any;
+}
+
+interface FetchFunction {
+ (
+ operation: GraphQLOperation,
+ variables: GraphQLVariables,
+ cacheConfig: GraphQLCacheConfig,
+ uploadables?: any
+ ): Observable;
+}
+
+function safeStringify(value: unknown) {
+ try {
+ return JSON.stringify(value);
+ } catch {
+ // If we can’t stringify (e.g., cyclic object), return a placeholder
+ return '"[unserializable]"';
+ }
+}
+
+function createRelayObserver(sanitizer?: Sanitizer>) {
+ return (app: App | null) => {
+ return (originalFetch: FetchFunction) => (operation: GraphQLOperation, variables: GraphQLVariables, cacheConfig: GraphQLCacheConfig, uploadables?: any): Observable => {
+ const startTime = Date.now();
+ const observable = originalFetch(operation, variables, cacheConfig, uploadables);
+
+ if (!app || !app.active()) {
+ return observable;
+ }
+
+ return new Observable(observer =>
+ observable.subscribe({
+ next: (data: any) => {
+ const duration = Date.now() - startTime;
+ const opName = operation.name;
+ const opKind = operation.operationKind;
+ const vars = JSON.stringify(sanitizer ? sanitizer(variables) : variables);
+ if (data.errors && data.errors.length > 0) {
+ const opResp = safeStringify(sanitizer ? sanitizer(data.errors) : data.errors);
+ app.send(Messages.GraphQL(
+ opKind,
+ `ERROR: ${opName}`,
+ vars,
+ opResp,
+ duration
+ ));
+ } else {
+ const opResp = safeStringify(sanitizer ? sanitizer(data) : data);
+ app.send(Messages.GraphQL(
+ opKind,
+ opName,
+ vars,
+ opResp,
+ duration
+ ));
+ }
+ observer.next(data);
+ },
+ error: err => {
+ const duration = Date.now() - startTime;
+ const opName = 'ERROR: ' + operation.name;
+ const opKind = operation.operationKind;
+ const vars = safeStringify(sanitizer ? sanitizer(variables) : variables);
+ const opResp = safeStringify(err);
+ app.send(Messages.GraphQL(opKind, opName, vars, opResp, duration));
+ observer.error(err);
+ },
+ complete: () => {
+ observer.complete();
+ }
+ })
+ )
+ }
+ }
+}
const createRelayMiddleware = (sanitizer?: Sanitizer>) => {
return (app: App | null): Middleware => {
@@ -52,4 +141,4 @@ function getMessage(
return Messages.GraphQL(opKind, opName, vars, opResp, duration);
}
-export default createRelayMiddleware;
+export { createRelayMiddleware, createRelayObserver };
diff --git a/tracker/tracker-graphql/tsconfig.json b/tracker/tracker-graphql/tsconfig.json
index 31699449b..82ef5c790 100644
--- a/tracker/tracker-graphql/tsconfig.json
+++ b/tracker/tracker-graphql/tsconfig.json
@@ -3,11 +3,12 @@
"noImplicitThis": true,
"strictNullChecks": true,
"alwaysStrict": true,
- "target": "es6",
- "module": "es6",
+ "target": "es2020",
+ "module": "ESNext",
"moduleResolution": "node",
"declaration": true,
"outDir": "./lib",
"allowSyntheticDefaultImports": true
- }
+ },
+ "include": ["src/**/*.ts"],
}
diff --git a/tracker/tracker/.yarn/install-state.gz b/tracker/tracker/.yarn/install-state.gz
index 7c84f37ef..773e1c417 100644
Binary files a/tracker/tracker/.yarn/install-state.gz and b/tracker/tracker/.yarn/install-state.gz differ
diff --git a/tracker/tracker/package.json b/tracker/tracker/package.json
index 625da24a7..d0c141ac7 100644
--- a/tracker/tracker/package.json
+++ b/tracker/tracker/package.json
@@ -1,7 +1,7 @@
{
"name": "@openreplay/tracker",
"description": "The OpenReplay tracker main package",
- "version": "15.0.3",
+ "version": "15.0.4",
"keywords": [
"logging",
"replay"
diff --git a/tracker/tracker/src/main/app/guards.ts b/tracker/tracker/src/main/app/guards.ts
index 11edb1dd1..f7a7065d3 100644
--- a/tracker/tracker/src/main/app/guards.ts
+++ b/tracker/tracker/src/main/app/guards.ts
@@ -4,7 +4,13 @@ export function isNode(sth: any): sth is Node {
}
export function isSVGElement(node: Element): node is SVGElement {
- return node.namespaceURI === 'http://www.w3.org/2000/svg'
+ return (
+ node.namespaceURI === 'http://www.w3.org/2000/svg' || node.localName === 'svg'
+ )
+}
+
+export function isUseElement(node: Element): node is SVGUseElement {
+ return node.localName === 'use'
}
export function isElementNode(node: Node): node is Element {
diff --git a/tracker/tracker/src/main/app/observer/observer.ts b/tracker/tracker/src/main/app/observer/observer.ts
index 51eeb6d48..3c2b3bd68 100644
--- a/tracker/tracker/src/main/app/observer/observer.ts
+++ b/tracker/tracker/src/main/app/observer/observer.ts
@@ -9,6 +9,7 @@ import {
MoveNode,
RemoveNode,
UnbindNodes,
+ SetNodeAttribute,
} from '../messages.gen.js'
import App from '../index.js'
import {
@@ -16,10 +17,67 @@ import {
isTextNode,
isElementNode,
isSVGElement,
+ isUseElement,
hasTag,
isCommentNode,
} from '../guards.js'
+const iconCache = {}
+const domParser = new DOMParser()
+
+async function parseUseEl(useElement: SVGUseElement, mode: 'inline' | 'dataurl') {
+ try {
+ const href = useElement.getAttribute('xlink:href') || useElement.getAttribute('href')
+ if (!href) {
+ console.debug('Openreplay: xlink:href or href not found on