Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "lytics-dev-tools",
"version": "2.0.0",
"title": "Contentstack Data & Insights Dev Tools",
"title": "Lytics Dev Tools",
"description": "Advanced Chrome extension v2 for exploring, debugging, and enhancing the Lytics web SDK and APIs with powerful new features.",
"license": "MIT",
"repository": {
Expand Down
2 changes: 2 additions & 0 deletions src/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
declare const __TAGLINK_BUILD_ID__: string;

declare module 'virtual:reload-on-update-in-background-script' {
export const reloadOnUpdate: (watchPath: string) => void;
export default reloadOnUpdate;
Expand Down
2 changes: 1 addition & 1 deletion src/pages/content/modules/scriptInjection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe('Script Injection Module', () => {
injectScript();

expect(document.createElement).toHaveBeenCalledWith('script');
expect(mockScript.src).toBe('chrome-extension://test/tagLink.js');
expect(mockScript.src).toMatch(/^chrome-extension:\/\/test\/tagLink\.js\?v=/);
expect(document.documentElement.appendChild).toHaveBeenCalledWith(mockScript);
});

Expand Down
5 changes: 4 additions & 1 deletion src/pages/content/modules/scriptInjection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ export const injectScript = (scriptInjectedRef?: MutableRefObject<boolean>) => {

const script = document.createElement('script');

script.src = chrome.runtime.getURL('/tagLink.js');
// Cache-bust: tagLink.js keeps a stable filename (required by manifest.web_accessible_resources),
// so without a query param the browser can keep serving an old cached copy after an extension update.
const buildId = typeof __TAGLINK_BUILD_ID__ !== 'undefined' ? __TAGLINK_BUILD_ID__ : '';
script.src = `${chrome.runtime.getURL('/tagLink.js')}?v=${buildId}`;
script.onload = () => {
EmitLog({ name: 'content', payload: { msg: 'TagLink script loaded successfully' } });
if (scriptInjectedRef) {
Expand Down
2 changes: 1 addition & 1 deletion src/pages/sidepanel/sections/TagActivity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ const TagActivity = ({ textContent = appContent.tagActivity }: TagActivityProps)
{tagActivity.map((item, index) => (
<>
{item && (
<StyledAccordion disableGutters key={index}>
<StyledAccordion disableGutters key={item.ts} TransitionProps={{ unmountOnExit: true }}>
<AccordionSummary expandIcon={<ExpandMore />} aria-controls="panel1a-content" id={index.toString()}>
<Stack
direction={'row'}
Expand Down
79 changes: 57 additions & 22 deletions src/pages/tagLink/tagLink.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ class TagLinkInternal {
if (!this.jstagExist()) return;

window.jstag.call('entityReady', () => {
this.dispatchEvent('config', window.jstag.config);
try {
this.dispatchEvent('config', window.jstag.config);
} catch (err) {
this.emitLog('tag', { msg: 'failed to read config', error: String(err) });
}
});
}

Expand All @@ -79,8 +83,12 @@ class TagLinkInternal {

window.jstag.call('entityReady', entity => {
window.jstag.getid(id => {
entity.data._uid = id;
this.dispatchEvent('entity', entity);
try {
entity.data._uid = id;
this.dispatchEvent('entity', entity);
} catch (err) {
this.emitLog('tag', { msg: 'failed to read entity', error: String(err) });
}
});
});
}
Expand All @@ -100,27 +108,54 @@ class TagLinkInternal {
`);
}

safeClone(value, seen = new WeakSet()) {
if (value === null || typeof value !== 'object') {
return typeof value === 'bigint' ? value.toString() : value;
}

try {
if (value === value.window || value.self === value) return undefined;
} catch {
return undefined;
}

// Only walk plain data (object literals / arrays). Anything else - DOM nodes, Window,
// class instances, Map/Set, etc. - can reach huge or exotic graphs (e.g. a DOM element's
// ownerDocument/parentNode chain), so skip it rather than cloning it.
const proto = Object.getPrototypeOf(value);
const isPlainObject = proto === Object.prototype || proto === null;
if (!Array.isArray(value) && !isPlainObject) {
return undefined;
}

if (seen.has(value)) return undefined;
seen.add(value);

if (Array.isArray(value)) return value.map(v => this.safeClone(v, seen));

const out = {};
for (const key in value) {
try {
const v = value[key];
if (typeof v !== 'function') out[key] = this.safeClone(v, seen);
} catch {
// skip unreadable (cross-origin) property
}
}
return out;
}

dispatchEvent(name, payload) {
const safeStringify = obj => {
const seen = new WeakSet();

return JSON.stringify(obj, (key, value) => {
if (value !== null && typeof value === 'object') {
if (seen.has(value)) {
return undefined;
}
seen.add(value);
}
return value;
try {
const customEvent = new CustomEvent(name, {
detail: {
data: JSON.stringify(this.safeClone(payload)),
},
});
};

const customEvent = new CustomEvent(name, {
detail: {
data: safeStringify(payload),
},
});
document.dispatchEvent(customEvent);
document.dispatchEvent(customEvent);
} catch (err) {
this.emitLog('tag', { msg: 'failed to serialize payload', name, error: String(err) });
}
}
}

Expand Down
80 changes: 57 additions & 23 deletions src/pages/tagLink/tagLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,11 @@ class TagLinkInternal {
if (!this.jstagExist()) return;

(window as any).jstag.call('entityReady', () => {
this.dispatchEvent('config', (window as any).jstag.config);
try {
this.dispatchEvent('config', (window as any).jstag.config);
} catch (err) {
this.emitLog('tag', { msg: 'failed to read config', error: String(err) });
}
});
}

Expand All @@ -78,9 +82,12 @@ class TagLinkInternal {

(window as any).jstag.call('entityReady', entity => {
(window as any).jstag.getid(id => {
// this.emitLog('tag', 'got entity');
entity.data._uid = id;
this.dispatchEvent('entity', entity);
try {
entity.data._uid = id;
this.dispatchEvent('entity', entity);
} catch (err) {
this.emitLog('tag', { msg: 'failed to read entity', error: String(err) });
}
});
});
}
Expand All @@ -100,27 +107,54 @@ class TagLinkInternal {
`);
}

safeClone(value: any, seen = new WeakSet()): any {
if (value === null || typeof value !== 'object') {
return typeof value === 'bigint' ? value.toString() : value;
}

try {
if (value === value.window || value.self === value) return undefined;
} catch {
return undefined;
}

// Only walk plain data (object literals / arrays). Anything else - DOM nodes, Window,
// class instances, Map/Set, etc. - can reach huge or exotic graphs (e.g. a DOM element's
// ownerDocument/parentNode chain), so skip it rather than cloning it.
const proto = Object.getPrototypeOf(value);
const isPlainObject = proto === Object.prototype || proto === null;
if (!Array.isArray(value) && !isPlainObject) {
return undefined;
}

if (seen.has(value)) return undefined;
seen.add(value);

if (Array.isArray(value)) return value.map(v => this.safeClone(v, seen));

const out: Record<string, any> = {};
for (const key in value) {
try {
const v = value[key];
if (typeof v !== 'function') out[key] = this.safeClone(v, seen);
} catch {
// skip unreadable (cross-origin) property
}
}
return out;
}

dispatchEvent(name: string, payload: any) {
const safeStringify = (obj: any) => {
const seen = new WeakSet();

return JSON.stringify(obj, (key, value) => {
if (value !== null && typeof value === 'object') {
if (seen.has(value)) {
return undefined;
}
seen.add(value);
}
return value;
try {
const customEvent = new CustomEvent(name, {
detail: {
data: JSON.stringify(this.safeClone(payload)),
},
});
};

const customEvent = new CustomEvent(name, {
detail: {
data: safeStringify(payload),
},
});
document.dispatchEvent(customEvent);
document.dispatchEvent(customEvent);
} catch (err) {
this.emitLog('tag', { msg: 'failed to serialize payload', name, error: String(err) });
}
}
}

Expand Down
5 changes: 4 additions & 1 deletion src/shared/storages/tabStateStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { EmitLog } from '@src/shared/components/EmitLog';
import { EventModel } from '@src/shared/models/eventModel';

const TabStateStorageKey = 'domainStates';
const MAX_TAG_ACTIVITY_EVENTS = 200;

export interface DomainState {
domain: string;
Expand Down Expand Up @@ -129,7 +130,9 @@ const domainStateStore: DomainStateStorage = {
allStates[domain] = createDefaultDomainState(domain);
}

allStates[domain].tagActivity = [...(allStates[domain].tagActivity || []), event];
const nextActivity = [...(allStates[domain].tagActivity || []), event];
allStates[domain].tagActivity =
nextActivity.length > MAX_TAG_ACTIVITY_EVENTS ? nextActivity.slice(-MAX_TAG_ACTIVITY_EVENTS) : nextActivity;
allStates[domain].lastUpdated = Date.now();

await storage.set(allStates);
Expand Down
3 changes: 3 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const assetsDir = resolve(srcDir, 'assets');
const publicDir = resolve(rootDir, 'public');

export default defineConfig({
define: {
__TAGLINK_BUILD_ID__: JSON.stringify(Date.now()),
},
resolve: {
alias: {
'@root': rootDir,
Expand Down
Loading