Skip to content

Commit 952e5ed

Browse files
committed
MP improvements
1 parent 53583c2 commit 952e5ed

7 files changed

Lines changed: 269 additions & 51 deletions

File tree

browser-extension/src/content/dom/selectors.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
export const POST_CONTENT_SELECTOR = '[data-ad-comet-preview="message"]';
44

5-
export const ALLOWED_GROUP_NAMES: string[] = [
6-
'Artificial Intelligence & Deep Learning Memes For Back-propagated Poets',
7-
'Social Media for People',
8-
];
5+
// Allowed Facebook group IDs. The extension activates only for these groups.
6+
// Checked directly from the URL (e.g., /groups/<id>/...).
7+
export const ALLOWED_GROUP_IDS: string[] = ['1280044857038905', '1638417209555402'];

browser-extension/src/content/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { FacebookPostObserver } from './observer';
33
import { FloatingChatWindow } from './ui/components/ChatWindow';
44
import { metricsManager } from './metrics/MetricsManager';
55
import { analytics } from '@/shared/analytics';
6+
import { isInAllowedGroupNow } from '@/content/utils/group';
67

78
declare const __DEV__: boolean;
89
if (!__DEV__) {
@@ -16,8 +17,10 @@ if (!__DEV__) {
1617
// Entry bootstrap: initialize metrics, observer and chat UI
1718
(async () => {
1819
try {
19-
// Initialize Mixpanel as early as possible
20-
analytics.init();
20+
// Enable Mixpanel analytics only for allowed groups
21+
const allowed = isInAllowedGroupNow();
22+
analytics.setEnabled(allowed);
23+
if (allowed) analytics.init();
2124

2225
// Initialize metrics collection first
2326
await metricsManager.initialize();

browser-extension/src/content/metrics/MetricsCollector.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export class MetricsCollector {
2525
private lastScrollY: number = 0;
2626
private scrollSpeeds: number[] = [];
2727
private postViewTimes: Map<string, number> = new Map();
28+
private postCumulativeView: Map<string, number> = new Map();
2829

2930
constructor(config: MetricsConfig) {
3031
this.config = config;
@@ -75,6 +76,17 @@ export class MetricsCollector {
7576
// Post entered viewport
7677
this.postViewTimes.set(postId, currentTime);
7778

79+
// Fire explicit read start
80+
this.addEvent({
81+
type: 'post_read_start',
82+
category: 'interaction',
83+
metadata: {
84+
postId,
85+
intersectionRatio: entry.intersectionRatio,
86+
},
87+
clientTimestamp: new Date().toISOString(),
88+
});
89+
7890
this.addEvent({
7991
type: 'post_viewport_enter',
8092
category: 'interaction',
@@ -96,6 +108,11 @@ export class MetricsCollector {
96108
const viewportTime = currentTime - startTime;
97109
this.postViewTimes.delete(postId);
98110

111+
// Update cumulative time
112+
const prev = this.postCumulativeView.get(postId) || 0;
113+
const total = prev + viewportTime;
114+
this.postCumulativeView.set(postId, total);
115+
99116
this.addEvent({
100117
type: 'post_viewport_exit',
101118
category: 'interaction',
@@ -106,6 +123,19 @@ export class MetricsCollector {
106123
},
107124
clientTimestamp: new Date().toISOString(),
108125
});
126+
127+
// Fire explicit read end with cumulative total so far
128+
this.addEvent({
129+
type: 'post_read_end',
130+
category: 'interaction',
131+
value: viewportTime,
132+
metadata: {
133+
postId,
134+
sessionViewMs: viewportTime,
135+
cumulativeViewMs: total,
136+
},
137+
clientTimestamp: new Date().toISOString(),
138+
});
109139
}
110140
}
111141
});

browser-extension/src/content/observer.ts

Lines changed: 120 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,8 @@ import {
1010
sendChatSessionMetrics,
1111
} from '@/content/messaging';
1212
import { metricsManager } from './metrics/MetricsManager';
13-
import {
14-
POST_CONTENT_SELECTOR as POST_SELECTOR,
15-
ALLOWED_GROUP_NAMES as GROUPS,
16-
} from '@/content/dom/selectors';
13+
import { POST_CONTENT_SELECTOR as POST_SELECTOR } from '@/content/dom/selectors';
14+
import { isInAllowedGroupNow } from '@/content/utils/group';
1715
import { ChatMessage } from '@/content/types';
1816
// Chat session metrics state attached to chat window elements
1917
interface ChatMetrics {
@@ -49,29 +47,14 @@ export class FacebookPostObserver {
4947
/** Selector for extracting post content */
5048
private readonly POST_CONTENT_SELECTOR = POST_SELECTOR;
5149

52-
/** List of allowed Facebook group names where the extension should work */
53-
private readonly ALLOWED_GROUP_NAMES = GROUPS;
54-
5550
/**
5651
* Checks if the current page is in an allowed Facebook group
5752
* @returns boolean indicating if the extension should be active
5853
*/
5954
private isInAllowedGroup(): boolean {
60-
const currentGroupName = this.getCurrentGroupName();
61-
62-
if (!currentGroupName) {
63-
// Not in a group, extension should NOT work
64-
return false;
65-
}
66-
67-
const isAllowed = this.ALLOWED_GROUP_NAMES.some(
68-
allowedName =>
69-
currentGroupName.toLowerCase().includes(allowedName.toLowerCase()) ||
70-
allowedName.toLowerCase().includes(currentGroupName.toLowerCase())
71-
);
72-
73-
log(`[AI-Slop] Group filtering - Current group: "${currentGroupName}", Allowed: ${isAllowed}`);
74-
return isAllowed;
55+
const allowed = isInAllowedGroupNow();
56+
log(`[AI-Slop] Group filtering - Allowed: ${allowed}`);
57+
return allowed;
7558
}
7659

7760
/**
@@ -543,6 +526,11 @@ export class FacebookPostObserver {
543526
// Allow posts with no text content if they have media (media-only posts)
544527
if (trimmedContent.length === 0 && !hasMedia) {
545528
log(`[AI-Slop] ⏭️ Post ${postId} has no content and no media, skipping analysis`);
529+
metricsManager.trackEvent({
530+
type: 'post_skipped_empty',
531+
category: 'post',
532+
metadata: { postId },
533+
});
546534
return;
547535
}
548536

@@ -562,6 +550,11 @@ export class FacebookPostObserver {
562550
log(
563551
`[AI-Slop] ⏭️ Post ${postId} has repetitive text content (${trimmedContent.length} chars, ${facebookCount} "Facebook" occurrences), skipping analysis`
564552
);
553+
metricsManager.trackEvent({
554+
type: 'post_skipped_repetitive',
555+
category: 'post',
556+
metadata: { postId, length: trimmedContent.length, facebookCount },
557+
});
565558
return;
566559
}
567560
}
@@ -619,12 +612,88 @@ export class FacebookPostObserver {
619612

620613
// Store analysis result and inject icon
621614
this.injectFactCheckIcon(postElement, postId, content, response);
615+
616+
// Setup media/video tracking inside this post (if present)
617+
this.setupVideoTracking(postElement, postId);
622618
} catch (error) {
623619
logError(`[AI-Slop] ❌ Error analyzing post ${postId}:`, error);
624620
// Don't show icon if analysis fails to avoid confusion
625621
}
626622
}
627623

624+
/**
625+
* Tracks video interactions inside a post
626+
*/
627+
private videoState = new WeakMap<HTMLElement, { lastPlayTs: number | null; playAccumMs: number; lastProgressSentAt: number; lastTime: number }>();
628+
629+
private setupVideoTracking(postElement: HTMLElement, postId: string): void {
630+
const videos = postElement.querySelectorAll('video');
631+
videos.forEach(v => {
632+
const el = v as HTMLVideoElement & { _aiSlopTracked?: boolean };
633+
if (el._aiSlopTracked) return;
634+
el._aiSlopTracked = true;
635+
636+
this.videoState.set(el, { lastPlayTs: null, playAccumMs: 0, lastProgressSentAt: 0, lastTime: 0 });
637+
638+
el.addEventListener('play', () => {
639+
const st = this.videoState.get(el);
640+
if (!st) return;
641+
st.lastPlayTs = Date.now();
642+
metricsManager.trackEvent({
643+
type: 'video_play',
644+
category: 'video',
645+
metadata: { postId, duration: Math.round(el.duration || 0) },
646+
});
647+
});
648+
649+
el.addEventListener('pause', () => {
650+
const st = this.videoState.get(el);
651+
if (!st) return;
652+
if (st.lastPlayTs) {
653+
const delta = Date.now() - st.lastPlayTs;
654+
st.playAccumMs += delta;
655+
st.lastPlayTs = null;
656+
metricsManager.trackEvent({
657+
type: 'video_pause',
658+
category: 'video',
659+
metadata: { postId, playSessionMs: delta, totalPlayedMs: st.playAccumMs, currentTime: Math.round(el.currentTime) },
660+
});
661+
}
662+
});
663+
664+
el.addEventListener('ended', () => {
665+
const st = this.videoState.get(el);
666+
if (!st) return;
667+
if (st.lastPlayTs) {
668+
st.playAccumMs += Date.now() - st.lastPlayTs;
669+
st.lastPlayTs = null;
670+
}
671+
const percent = el.duration ? Math.min(100, Math.round((el.currentTime / el.duration) * 100)) : 0;
672+
metricsManager.trackEvent({
673+
type: 'video_end',
674+
category: 'video',
675+
metadata: { postId, totalPlayedMs: st.playAccumMs, percentWatched: percent },
676+
});
677+
});
678+
679+
el.addEventListener('timeupdate', () => {
680+
const st = this.videoState.get(el);
681+
if (!st) return;
682+
const now = Date.now();
683+
if (now - st.lastProgressSentAt > 5000) {
684+
st.lastProgressSentAt = now;
685+
st.lastTime = el.currentTime;
686+
const percent = el.duration ? Math.min(100, Math.round((el.currentTime / el.duration) * 100)) : 0;
687+
metricsManager.trackEvent({
688+
type: 'video_progress',
689+
category: 'video',
690+
metadata: { postId, currentTime: Math.round(el.currentTime), percent },
691+
});
692+
}
693+
});
694+
});
695+
}
696+
628697
/**
629698
* Generates a unique ID for a post based on Facebook's URL structure
630699
* Priority: URL-based ID > content-based fallback > DOM-based fallback
@@ -1266,6 +1335,11 @@ export class FacebookPostObserver {
12661335
try {
12671336
targetElement.appendChild(iconContainer);
12681337
log(`[AI-Slop] ✅ Icon injected successfully for post ${postId} with consistent positioning`);
1338+
metricsManager.trackEvent({
1339+
type: 'icon_injected',
1340+
category: 'interaction',
1341+
metadata: { postId },
1342+
});
12691343
} catch (error) {
12701344
logError(`[AI-Slop] ❌ Failed to append icon for post ${postId}:`, error);
12711345

@@ -1280,6 +1354,7 @@ export class FacebookPostObserver {
12801354
}
12811355
container.appendChild(iconContainer);
12821356
log(`[AI-Slop] ✅ Icon injected using fallback container`);
1357+
metricsManager.trackEvent({ type: 'icon_injected_fallback', category: 'interaction', metadata: { postId } });
12831358
break;
12841359
} catch {
12851360
continue;
@@ -1337,6 +1412,7 @@ export class FacebookPostObserver {
13371412
}
13381413

13391414
this.showChatOverlay(postElement, postId, content, analysisResult);
1415+
metricsManager.trackEvent({ type: 'chat_overlay_open', category: 'chat', metadata: { postId } });
13401416
}
13411417

13421418
/**
@@ -1627,6 +1703,7 @@ export class FacebookPostObserver {
16271703

16281704
// Close chat window fast; send metrics in background
16291705
closeButton?.addEventListener('click', () => {
1706+
metricsManager.trackEvent({ type: 'chat_overlay_close', category: 'chat' });
16301707
const metrics =
16311708
(chatWindow as unknown as HTMLElement & { _chatMetrics?: ChatMetrics })._chatMetrics ||
16321709
null;
@@ -1648,6 +1725,12 @@ export class FacebookPostObserver {
16481725

16491726
// Minimize chat window
16501727
minimizeButton?.addEventListener('click', () => {
1728+
const minimized = chatWindow.classList.contains('minimized');
1729+
metricsManager.trackEvent({
1730+
type: 'chat_overlay_minimize_toggle',
1731+
category: 'chat',
1732+
label: minimized ? 'restore' : 'minimize',
1733+
});
16511734
if (chatWindow.classList.contains('minimized')) {
16521735
chatWindow.classList.remove('minimized');
16531736
} else {
@@ -1664,6 +1747,12 @@ export class FacebookPostObserver {
16641747
};
16651748

16661749
sendButton?.addEventListener('click', sendMessage);
1750+
chatInput?.addEventListener('focus', () => {
1751+
metricsManager.trackEvent({ type: 'chat_input_focus', category: 'chat' });
1752+
});
1753+
chatInput?.addEventListener('blur', () => {
1754+
metricsManager.trackEvent({ type: 'chat_input_blur', category: 'chat' });
1755+
});
16671756
chatInput?.addEventListener('keypress', e => {
16681757
if (e.key === 'Enter') {
16691758
sendMessage();
@@ -1672,6 +1761,7 @@ export class FacebookPostObserver {
16721761

16731762
// Ignore analysis
16741763
ignoreButton?.addEventListener('click', () => {
1764+
metricsManager.trackEvent({ type: 'chat_ignore_analysis', category: 'chat' });
16751765
const analysisResult = chatWindow.querySelector('.analysis-result');
16761766
if (analysisResult) {
16771767
analysisResult.innerHTML = `
@@ -1960,6 +2050,8 @@ export class FacebookPostObserver {
19602050
// Add class for potential drag-specific styles
19612051
chatWindow.classList.add('dragging');
19622052

2053+
metricsManager.trackEvent({ type: 'chat_overlay_drag_start', category: 'chat' });
2054+
19632055
e.preventDefault(); // Prevent default browser drag behavior
19642056
}
19652057
};
@@ -1983,6 +2075,12 @@ export class FacebookPostObserver {
19832075
chatWindow.style.userSelect = '';
19842076
chatWindow.classList.remove('dragging');
19852077

2078+
metricsManager.trackEvent({
2079+
type: 'chat_overlay_drag_end',
2080+
category: 'chat',
2081+
metadata: { x: xOffset, y: yOffset },
2082+
});
2083+
19862084
// Cancel any pending animation frame
19872085
if (animationId) {
19882086
cancelAnimationFrame(animationId);

0 commit comments

Comments
 (0)