Skip to content

Commit aa482b8

Browse files
anupsingh244onkarj-47
authored andcommitted
feat: cross-tab pin domain implemented along with few fixes (#98)
1 parent e4d53ac commit aa482b8

18 files changed

Lines changed: 1959 additions & 391 deletions

src/pages/background/index.ts

Lines changed: 119 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import 'webextension-polyfill';
22

33
import { currentTabAutoDetector } from '@root/src/pages/background/currentTabAutoDetector';
4+
import { createNetworkTrafficHandler } from '@root/src/pages/background/lyticsNetworkHandler';
45
import { EmitLog } from '@root/src/shared/components/EmitLog';
5-
import { EventModel } from '@src/shared/models/eventModel';
66
import entityStore from '@src/shared/storages/entityStorage';
77
import { domainStore } from '@src/shared/storages/extensionDomainStorage';
88
import extensionStateStorage from '@src/shared/storages/extensionStateStorage';
99
import tagActivityStore from '@src/shared/storages/tagActivityStorage';
1010
import tagConfigStore from '@src/shared/storages/tagConfigStorage';
1111
import { autoDetectedDomainsStore } from '@src/shared/storages/autoDetectedDomainsStorage';
12+
import domainStateStore from '@src/shared/storages/tabStateStorage';
1213

1314
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(error => {
1415
EmitLog({ name: 'background', payload: { msg: 'Failed to set side panel behavior', error: error.message } });
@@ -152,154 +153,9 @@ domainStore.subscribe(() => {
152153
// --------------------------------------------------------
153154
// Handle Listen for Requests to lytics.io
154155
// --------------------------------------------------------
155-
const parseURL = (url: string, body: any, type: string): EventModel => {
156-
// Parse URL
157-
const parsedURL = new URL(url);
158-
const parsedBody = new URLSearchParams(body);
159-
160-
// Get URL components
161-
const protocol = parsedURL.protocol; // e.g., 'https:'
162-
const host = parsedURL.host; // e.g., 'example.com'
163-
const pathname = parsedURL.pathname; // e.g., '/path/to/page'
164-
const search = parsedURL.search; // e.g., '?param1=value1&param2=value2'
165-
166-
// Parse query parameters into an object
167-
const queryParams = new URLSearchParams(search);
168-
const queryParamObj: Record<string, string> = {};
169-
queryParams.forEach((value, key) => {
170-
queryParamObj[key] = value;
171-
});
172-
173-
const parsedBodyDataObj = {};
174-
if (parsedBody) {
175-
parsedBody.forEach((value, key) => {
176-
parsedBodyDataObj[key] = decodeURIComponent(value);
177-
});
178-
}
179-
180-
let ts: string;
181-
if (queryParamObj.ts) {
182-
ts = queryParamObj.ts;
183-
} else if ((parsedBodyDataObj as any).ts) {
184-
ts = (parsedBodyDataObj as any).ts;
185-
} else {
186-
ts = Date.now().toString();
187-
}
188-
189-
// Assign description based on type
190-
let description: string;
191-
switch (type) {
192-
case 'load-js-tag':
193-
description =
194-
'Loaded the core Lytics JavaScript SDK. Used for collecting data and surfacing the active visitors profile back to the browser.';
195-
break;
196-
case 'load-profile':
197-
description = 'Loaded the active visitors full profile from Lytics.';
198-
break;
199-
case 'collect-data':
200-
description = 'Collected data via jstag.send based on visitor activity.';
201-
break;
202-
case 'load-pathfora-tag':
203-
description = 'Loaded the Pathfora JavaScript SDK for advanced onsite personalization.';
204-
break;
205-
case 'load-pathfora-css':
206-
description =
207-
'Loaded the default Pathfora JavaScript SDK styles to ensure modals and inline widgets are styled correctly.';
208-
break;
209-
case 'load-experience-config':
210-
description = 'Loaded the experience configurations from the Lytics Experience Engine.';
211-
break;
212-
case 'load-campaign-config':
213-
description =
214-
'Loaded the legacy campaign configurations from Lytics. This feature is being deprecated. Reach out to support for additional guidance.';
215-
break;
216-
default:
217-
description = `Communicated with Lytics via a generic or unhandled request (${type}).`;
218-
}
219-
220-
return {
221-
protocol,
222-
host,
223-
pathname,
224-
queryParamObj,
225-
parsedBodyDataObj,
226-
ts: parseInt(ts),
227-
type,
228-
description,
229-
} as EventModel;
230-
};
231-
232-
const handleNetworkTraffic = details => {
233-
try {
234-
// get the active tab url that generated the request
235-
const activeTabUrl = details.initiator;
236-
if (!activeTabUrl) {
237-
return;
238-
}
239-
240-
// parse the active tab url
241-
const activeDomain = new URL(activeTabUrl).hostname;
242-
243-
// if the active domain matches the sticky domain, then we can proceed with existing logic
244-
if (activeDomain !== stickyDomain) {
245-
EmitLog({ name: 'background', payload: { msg: `Request from non-sticky domain <${activeDomain}>` } });
246-
return;
247-
}
248-
249-
const url = details.url;
250-
let postData;
251-
let result: EventModel;
252-
253-
switch (true) {
254-
case url.includes('/api/tag/'):
255-
EmitLog({ name: 'background', payload: { msg: 'Called Core Tag', url: url } });
256-
result = parseURL(url, {}, 'load-js-tag');
257-
break;
258-
259-
case url.includes('/api/personalize/'):
260-
EmitLog({ name: 'background', payload: { msg: 'Called Entity API', url: url } });
261-
result = parseURL(url, {}, 'load-profile');
262-
break;
263-
264-
case url.includes('/c/'):
265-
EmitLog({ name: 'background', payload: { msg: 'Collection API', url: url } });
266-
if (details.requestBody) {
267-
postData = details?.requestBody?.formData?._js[0];
268-
}
269-
result = parseURL(url, postData || {}, 'collect-data');
270-
break;
271-
272-
case url.includes('/static/pathfora.min.js'):
273-
EmitLog({ name: 'background', payload: { msg: 'Called Pathfora Tag', url: url } });
274-
result = parseURL(url, {}, 'load-pathfora-tag');
275-
break;
276-
277-
case url.includes('static/pathfora.min.css'):
278-
EmitLog({ name: 'background', payload: { msg: 'Called Pathfora Styles', url: url } });
279-
result = parseURL(url, {}, 'load-pathfora-css');
280-
break;
281-
282-
case url.includes('/experience/candidate'):
283-
EmitLog({ name: 'background', payload: { msg: 'Called Experience Config API', url: url } });
284-
result = parseURL(url, {}, 'load-experience-config');
285-
break;
286-
287-
case url.includes('/api/program/campaign/config'):
288-
EmitLog({ name: 'background', payload: { msg: 'Called Legacy Campaign Config API', url: url } });
289-
result = parseURL(url, {}, 'load-campaign-config');
290-
break;
291-
292-
default:
293-
EmitLog({ name: 'background', payload: { msg: 'Unhandled API Requset', url: url } });
294-
}
295-
tagActivityStore.add(JSON.stringify(result));
296-
} catch (error) {
297-
EmitLog({
298-
name: 'background',
299-
payload: { msg: 'Error in handleNetworkTraffic', error: error.message, url: details?.url },
300-
});
301-
}
302-
};
156+
const handleNetworkTraffic = createNetworkTrafficHandler({
157+
getStickyDomain: () => stickyDomain,
158+
});
303159

304160
// --------------------------------------------------------
305161
// Handle Auto-Detection Messages
@@ -469,6 +325,78 @@ chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
469325
}
470326
sendResponse({ success: true });
471327
}
328+
329+
// Handle tagConfig save from content script
330+
if (message.action === 'saveTagConfig') {
331+
const tabId = sender.tab?.id?.toString();
332+
const tabUrl = sender.tab?.url;
333+
if (tabId && tabUrl && message.payload) {
334+
const domain = new URL(tabUrl).hostname;
335+
336+
// Parse the payload if it's a string (tagLink sends stringified data)
337+
let parsedConfig = message.payload;
338+
if (typeof message.payload === 'string') {
339+
try {
340+
parsedConfig = JSON.parse(message.payload);
341+
} catch (error) {
342+
EmitLog({
343+
name: 'background',
344+
payload: { msg: `Error parsing tag config`, error: error.message },
345+
});
346+
sendResponse({ success: false, error: 'Invalid JSON' });
347+
return;
348+
}
349+
}
350+
351+
// Save to domain state (shared across all tabs on this domain)
352+
await domainStateStore.updateDomainState(domain, {
353+
tagConfig: parsedConfig,
354+
});
355+
EmitLog({
356+
name: 'background',
357+
payload: {
358+
msg: `Tag config saved for domain ${domain}`,
359+
hasData: Object.keys(parsedConfig).length > 0,
360+
tabId,
361+
},
362+
});
363+
}
364+
sendResponse({ success: true });
365+
}
366+
367+
// Handle entity save from content script
368+
if (message.action === 'saveEntity') {
369+
const tabId = sender.tab?.id?.toString();
370+
const tabUrl = sender.tab?.url;
371+
if (tabId && tabUrl && message.payload) {
372+
const domain = new URL(tabUrl).hostname;
373+
374+
// Parse the payload if it's a string (tagLink sends stringified data)
375+
let parsedProfile = message.payload;
376+
if (typeof message.payload === 'string') {
377+
try {
378+
parsedProfile = JSON.parse(message.payload);
379+
} catch (error) {
380+
EmitLog({
381+
name: 'background',
382+
payload: { msg: `Error parsing entity profile`, error: error.message },
383+
});
384+
sendResponse({ success: false, error: 'Invalid JSON' });
385+
return;
386+
}
387+
}
388+
389+
// Save to domain state (shared across all tabs on this domain)
390+
await domainStateStore.updateDomainState(domain, {
391+
profile: parsedProfile,
392+
});
393+
EmitLog({
394+
name: 'background',
395+
payload: { msg: `Entity profile saved for domain ${domain}`, tabId },
396+
});
397+
}
398+
sendResponse({ success: true });
399+
}
472400
} catch (error) {
473401
EmitLog({
474402
name: 'background',
@@ -480,3 +408,45 @@ chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
480408
// Return true for async operations
481409
return true;
482410
});
411+
412+
// --------------------------------------------------------
413+
// Per-Domain Storage Cleanup
414+
// --------------------------------------------------------
415+
416+
// Unregister tab from domain when tab is closed
417+
chrome.tabs.onRemoved.addListener(async (tabId: number) => {
418+
try {
419+
const tabIdStr = tabId.toString();
420+
421+
// Get all domains and unregister this tab from each
422+
const allDomains = await domainStateStore.getAllDomains();
423+
for (const domain of allDomains) {
424+
await domainStateStore.unregisterTab(domain, tabIdStr);
425+
}
426+
427+
// Immediately cleanup domains with no active tabs
428+
await domainStateStore.cleanupInactiveDomains();
429+
430+
EmitLog({
431+
name: 'background',
432+
payload: { msg: `Tab ${tabIdStr} closed, unregistered and cleaned up inactive domains` },
433+
});
434+
} catch (error) {
435+
EmitLog({
436+
name: 'background',
437+
payload: { msg: 'Error cleaning up closed tab', error: error.message, tabId },
438+
});
439+
}
440+
});
441+
442+
// Periodic cleanup of inactive domains (every 60 seconds)
443+
setInterval(async () => {
444+
try {
445+
await domainStateStore.cleanupInactiveDomains();
446+
} catch (error) {
447+
EmitLog({
448+
name: 'background',
449+
payload: { msg: 'Error in periodic domain cleanup', error: error.message },
450+
});
451+
}
452+
}, 60000);

0 commit comments

Comments
 (0)