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: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
"@mui/material": "^5.15.1",
"@mui/styles": "^5.15.2",
"@mui/x-charts": "^6.18.4",
"@tanstack/react-query": "^5.90.2",
"@tanstack/react-query-devtools": "^5.90.2",
"construct-style-sheets-polyfill": "3.1.0",
"date-fns": "^3.0.6",
"moment": "^2.30.1",
Expand Down
18 changes: 13 additions & 5 deletions src/pages/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@ import 'webextension-polyfill';
import { currentTabAutoDetector } from '@root/src/pages/background/currentTabAutoDetector';
import { createNetworkTrafficHandler } from '@root/src/pages/background/lyticsNetworkHandler';
import { EmitLog } from '@root/src/shared/components/EmitLog';
import { autoDetectedDomainsStore } from '@src/shared/storages/autoDetectedDomainsStorage';
import entityStore from '@src/shared/storages/entityStorage';
import { domainStore } from '@src/shared/storages/extensionDomainStorage';
import extensionStateStorage from '@src/shared/storages/extensionStateStorage';
import domainStateStore from '@src/shared/storages/tabStateStorage';
import tagActivityStore from '@src/shared/storages/tagActivityStorage';
import tagConfigStore from '@src/shared/storages/tagConfigStorage';
import { autoDetectedDomainsStore } from '@src/shared/storages/autoDetectedDomainsStorage';
import domainStateStore from '@src/shared/storages/tabStateStorage';

chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(error => {
EmitLog({ name: 'background', payload: { msg: 'Failed to set side panel behavior', error: error.message } });
});
import { messageBroker } from '../../shared/message-broker';
import { IMessage } from '../../shared/message-broker/types';

chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(error => console.error(error));

chrome.tabs.onUpdated.addListener(async (tabId, info, tab) => {
if (!tab.url) return;
Expand Down Expand Up @@ -450,3 +451,10 @@ setInterval(async () => {
});
}
}, 60000);
messageBroker.handle('GET_CONFIG', async (message: IMessage) => {
const tabMessage: IMessage = {
key: 'GET_CONFIG',
};

return await messageBroker.sendToTab(message.payload.currentTabId, tabMessage);
});
206 changes: 206 additions & 0 deletions src/pages/content/modules/autoDetection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { EmitLog } from '@src/shared/components/EmitLog';

import {
isJstagAvailable,
notifyAutoDetectionFailed,
notifyAutoDetectionSuccess,
pollForJstag,
startAutoDetection,
} from './autoDetection';

vi.mock('@src/shared/components/EmitLog');

describe('autoDetection', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers();
delete (window as any).jstag;
global.chrome = {
runtime: {
sendMessage: vi.fn().mockResolvedValue({}),
},
storage: {
local: {
get: vi.fn().mockResolvedValue({ extensionState: true }),
set: vi.fn().mockResolvedValue(undefined),
},
},
} as any;
});

describe('isJstagAvailable', () => {
it('should return true when jstag is defined', () => {
(window as any).jstag = {};
expect(isJstagAvailable()).toBe(true);
});

it('should return false when jstag is undefined', () => {
delete (window as any).jstag;
expect(isJstagAvailable()).toBe(false);
});

it('should return false when jstag is null', () => {
(window as any).jstag = null;
expect(isJstagAvailable()).toBe(false);
});
});

describe('notifyAutoDetectionSuccess', () => {
it('should send message to background with correct payload', async () => {
const domain = 'example.com';
await notifyAutoDetectionSuccess(domain);

expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
action: 'recordDetection',
domain: domain,
confidence: 0.9,
});
expect(EmitLog).toHaveBeenCalledWith({
name: 'content',
payload: { msg: `Notified background of successful detection for: ${domain}` },
});
});

it('should handle sendMessage error gracefully', async () => {
const domain = 'example.com';
const error = new Error('Send failed');
(chrome.runtime.sendMessage as any).mockRejectedValue(error);

await notifyAutoDetectionSuccess(domain);

expect(EmitLog).toHaveBeenCalledWith({
name: 'content',
payload: { msg: 'Error notifying background of successful detection', error: error.message },
});
});
});

describe('notifyAutoDetectionFailed', () => {
it('should send failure message to background', async () => {
const domain = 'example.com';
await notifyAutoDetectionFailed(domain);

expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
action: 'autoDetectionFailed',
domain: domain,
retryCount: 0,
});
expect(EmitLog).toHaveBeenCalledWith({
name: 'content',
payload: { msg: `Notified background of failed detection for: ${domain}` },
});
});

it('should handle sendMessage error gracefully', async () => {
const domain = 'example.com';
const error = new Error('Send failed');
(chrome.runtime.sendMessage as any).mockRejectedValue(error);

await notifyAutoDetectionFailed(domain);

expect(EmitLog).toHaveBeenCalledWith({
name: 'content',
payload: { msg: 'Error notifying background of failed detection', error: error.message },
});
});
});

describe('pollForJstag', () => {
it('should detect jstag on first check', async () => {
const domain = 'example.com';
(window as any).jstag = {};

pollForJstag(domain);

await vi.runAllTimersAsync();

expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
action: 'recordDetection',
domain: domain,
confidence: 0.9,
});
});

it('should retry and eventually find jstag', async () => {
const domain = 'example.com';

pollForJstag(domain);

await vi.advanceTimersByTimeAsync(750);
expect(EmitLog).toHaveBeenCalledWith({
name: 'content',
payload: { msg: 'Auto-detection retry 1/5' },
});

(window as any).jstag = {};
await vi.advanceTimersByTimeAsync(750);

expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
action: 'recordDetection',
domain: domain,
confidence: 0.9,
});
});

it('should fail after max retries', async () => {
const domain = 'example.com';

pollForJstag(domain);

for (let i = 0; i < 5; i++) {
await vi.advanceTimersByTimeAsync(750);
}

expect(EmitLog).toHaveBeenCalledWith({
name: 'content',
payload: { msg: 'Auto-detection failed - no jstag found after max retries' },
});
expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
action: 'autoDetectionFailed',
domain: domain,
retryCount: 0,
});
});
});

describe('startAutoDetection', () => {
it('should detect jstag immediately if available', async () => {
const domain = 'example.com';
(window as any).jstag = {};

await startAutoDetection(domain);

expect(EmitLog).toHaveBeenCalledWith({
name: 'content',
payload: { msg: 'Lytics jstag found immediately' },
});
expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
action: 'recordDetection',
domain: domain,
confidence: 0.9,
});
});

it('should start polling if jstag not immediately available', async () => {
const domain = 'example.com';

await startAutoDetection(domain);

expect(EmitLog).toHaveBeenCalledWith({
name: 'content',
payload: { msg: `Starting auto-detection for domain: ${domain}` },
});

(window as any).jstag = {};
await vi.runAllTimersAsync();

expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
action: 'recordDetection',
domain: domain,
confidence: 0.9,
});
});
});
});
100 changes: 100 additions & 0 deletions src/pages/content/modules/autoDetection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { EmitLog } from '@src/shared/components/EmitLog';
import extensionStateStorage from '@src/shared/storages/extensionStateStorage';

export const isJstagAvailable = (): boolean => {
return typeof (window as any).jstag !== 'undefined' && (window as any).jstag !== null;
};

export const pollForJstag = (domain: string): void => {
let retryCount = 0;
const maxRetries = 5;
const retryInterval = 750;

const checkForJstag = async () => {
const isEnabled = await extensionStateStorage.get();
if (!isEnabled) {
EmitLog({ name: 'content', payload: { msg: 'Extension disabled - stopping auto-detection polling' } });
return;
}

retryCount++;

if (isJstagAvailable()) {
EmitLog({ name: 'content', payload: { msg: 'Lytics jstag detected during auto-detection' } });
await notifyAutoDetectionSuccess(domain);
return;
}

if (retryCount < maxRetries) {
EmitLog({ name: 'content', payload: { msg: `Auto-detection retry ${retryCount}/${maxRetries}` } });
setTimeout(checkForJstag, retryInterval);
} else {
EmitLog({ name: 'content', payload: { msg: 'Auto-detection failed - no jstag found after max retries' } });
await notifyAutoDetectionFailed(domain);
}
};

checkForJstag();
};

export const startAutoDetection = async (domain: string): Promise<void> => {
const isEnabled = await extensionStateStorage.get();
if (!isEnabled) {
EmitLog({ name: 'content', payload: { msg: 'Extension disabled - skipping auto-detection', domain } });
return;
}

EmitLog({ name: 'content', payload: { msg: `Starting auto-detection for domain: ${domain}` } });

if (isJstagAvailable()) {
EmitLog({ name: 'content', payload: { msg: 'Lytics jstag found immediately' } });
await notifyAutoDetectionSuccess(domain);
return;
}

pollForJstag(domain);
};

export const notifyAutoDetectionSuccess = async (domain: string): Promise<void> => {
const isEnabled = await extensionStateStorage.get();
if (!isEnabled) {
EmitLog({ name: 'content', payload: { msg: 'Extension disabled - not notifying detection success', domain } });
return;
}

try {
await chrome.runtime.sendMessage({
action: 'recordDetection',
domain: domain,
confidence: 0.9,
});
EmitLog({ name: 'content', payload: { msg: `Notified background of successful detection for: ${domain}` } });
} catch (error) {
EmitLog({
name: 'content',
payload: { msg: 'Error notifying background of successful detection', error: error.message },
});
}
};

export const notifyAutoDetectionFailed = async (domain: string): Promise<void> => {
const isEnabled = await extensionStateStorage.get();
if (!isEnabled) {
EmitLog({ name: 'content', payload: { msg: 'Extension disabled - not notifying detection failure', domain } });
return;
}

try {
await chrome.runtime.sendMessage({
action: 'autoDetectionFailed',
domain: domain,
retryCount: 0,
});
EmitLog({ name: 'content', payload: { msg: `Notified background of failed detection for: ${domain}` } });
} catch (error) {
EmitLog({
name: 'content',
payload: { msg: 'Error notifying background of failed detection', error: error.message },
});
}
};
Loading