Skip to content

Commit 476d8fc

Browse files
bradley-ericksonDrLynch
authored andcommitted
Updated extension to only connect when relevant tabs are present (ArgLab#235)
* updated extension to only connect when relevant tabs are present * updated urls * added terminate event to the server to close files
1 parent 671d680 commit 476d8fc

6 files changed

Lines changed: 151 additions & 60 deletions

File tree

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.0+2025.09.18T20.00.55.977Z.4d8e2b48.berickson.2025.document.restructure
1+
0.1.0+2025.09.15T18.58.19.025Z.cb6381ff.berickson.202509.extension.fixes

extension/writing-process/src/background.js

Lines changed: 83 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -28,34 +28,54 @@ import { localStorageInfo, sessionStorageInfo } from 'lo_event/lo_event/metadata
2828
// } /* and other async logic */);
2929
// websocketLogger( callback );
3030

31+
// Track which tabs currently have an active content script and the state of
32+
// our logger so that we only initialize logging when needed.
33+
const activeContentTabs = new Set();
34+
let loEventActive = false;
35+
let loggers = [];
36+
const manifestVersion = chrome.runtime.getManifest().version;
37+
38+
3139
// We are not sure if this should be done within `websocketLogger()`'s `init`
3240
// or one level up.
33-
const loggers = [
34-
consoleLogger(),
35-
websocketLogger(WEBSOCKET_SERVER_URL)
36-
]
37-
38-
loEvent.init(
39-
'org.mitros.writing_analytics',
40-
'0.01',
41-
loggers,
42-
{
43-
debugLevel: loEventDebug.LEVEL.SIMPLE,
44-
metadata: [
45-
browserInfo(),
46-
chromeAuth(),
47-
localStorageInfo(),
48-
sessionStorageInfo(),
49-
]
50-
}
51-
);
52-
loEvent.go();
41+
function startLogger () {
42+
if (loEventActive) return;
43+
loggers = [
44+
consoleLogger(),
45+
websocketLogger(WEBSOCKET_SERVER_URL)
46+
];
47+
loEvent.init(
48+
'org.mitros.writing_analytics',
49+
manifestVersion,
50+
loggers,
51+
{
52+
debugLevel: loEventDebug.LEVEL.SIMPLE,
53+
// TODO document what we have currently and what we want
54+
metadata: [
55+
browserInfo(),
56+
chromeAuth(),
57+
localStorageInfo(),
58+
sessionStorageInfo(),
59+
]
60+
}
61+
);
62+
loEvent.go();
63+
loEventActive = true;
64+
loEvent.logEvent('extension_loaded', {});
65+
logFromServiceWorker('Extension loaded');
66+
}
67+
68+
function stopLogger () {
69+
if (!loEventActive) return;
70+
loEvent.logEvent('terminate', {});
71+
loEventActive = false;
72+
}
5373

5474
// Function to serve as replacement for
5575
// chrome.extension.getBackgroundPage().console.log(event); because it is not allowed in V3
5676
// It logs the event to the console for debugging.
5777
function logFromServiceWorker(event) {
58-
console.log(event);
78+
console.log(event);
5979
}
6080

6181
function this_a_google_docs_save(request) {
@@ -69,7 +89,7 @@ function this_a_google_docs_save(request) {
6989
went from a conservative regexp to a liberal one. We should
7090
confirm this never catches extra requests, though.
7191
*/
72-
if(request.url.match(/.*:\/\/docs\.google\.com\/document\/(.*)\/save/i)) {
92+
if (request.url.match(/.*:\/\/docs\.google\.com\/document\/(.*)\/save/i)) {
7393
return true;
7494
}
7595
return false;
@@ -85,7 +105,7 @@ function this_a_google_docs_bind(request) {
85105
86106
https://stackoverflow.com/questions/6831916/is-it-possible-to-monitor-http-traffic-in-chrome-using-an-extension#6832018
87107
*/
88-
if(request.url.match(/.*:\/\/docs\.google\.com\/document\/(.*)\/bind/i)) {
108+
if (request.url.match(/.*:\/\/docs\.google\.com\/document\/(.*)\/bind/i)) {
89109
return true;
90110
}
91111
return false;
@@ -106,10 +126,29 @@ chrome.storage.sync.get(['process_server'], function(result) {
106126

107127
// Listen for the keystroke messages from the page script and forward to the server.
108128
chrome.runtime.onMessage.addListener(
109-
function(request, sender, sendResponse) {
110-
//chrome.extension.getBackgroundPage().console.log("Got message");
111-
//chrome.extension.getBackgroundPage().console.log(request);
112-
//console.log(sender);
129+
function (request, sender, sendResponse) {
130+
// Lifecycle messages from content scripts manage the logger state
131+
if (request?.type === 'content_script_ready') {
132+
if (sender.tab?.id !== undefined) {
133+
activeContentTabs.add(sender.tab.id);
134+
if (!loEventActive) {
135+
startLogger();
136+
}
137+
}
138+
return;
139+
} else if (request?.type === 'content_script_unloading') {
140+
if (sender.tab?.id !== undefined) {
141+
activeContentTabs.delete(sender.tab.id);
142+
if (activeContentTabs.size === 0) {
143+
stopLogger();
144+
}
145+
}
146+
return;
147+
}
148+
// Forward analytics events only when the logger is active
149+
if (!loEventActive) {
150+
return;
151+
}
113152
request['wa_source'] = 'client_page';
114153
loEvent.logEvent(request['event'], request);
115154
}
@@ -142,55 +181,59 @@ chrome.webRequest.onBeforeRequest.addListener(
142181
especially new pages. They do inject a lot of noise, though, and
143182
from there, being able to easily ignore these is nice.
144183
*/
145-
function(request) {
184+
function (request) {
185+
// No logger availaber
186+
if (!loEventActive) {
187+
return;
188+
}
146189
//chrome.extension.getBackgroundPage().console.log("Web request url:"+request.url);
147190
var formdata = {};
148191
let event;
149-
if(request.requestBody) {
192+
if (request.requestBody) {
150193
formdata = request.requestBody.formData;
151194
}
152-
if(!formdata) {
195+
if (!formdata) {
153196
formdata = {};
154197
}
155-
if(RAW_DEBUG) {
198+
if (RAW_DEBUG) {
156199
loEvent.logEvent('raw_http_request', {
157-
'url': request.url,
200+
'url': request.url,
158201
'form_data': formdata
159202
});
160203
}
161204

162-
if(this_a_google_docs_save(request)){
205+
if (this_a_google_docs_save(request)) {
163206
//chrome.extension.getBackgroundPage().console.log("Google Docs bundles "+request.url);
164207
try {
165208
/* We should think through which time stamps we should log. These are all subtly
166209
different: browser event versus request timestamp, as well as user time zone
167210
versus GMT. */
168211
event = {
169-
'doc_id': googledocs_id_from_url(request.url),
212+
'doc_id': googledocs_id_from_url(request.url),
170213
'url': request.url,
171214
'bundles': JSON.parse(formdata.bundles),
172215
'rev': formdata.rev,
173216
'timestamp': parseInt(request.timeStamp, 10)
174217
};
175218
logFromServiceWorker(event);
176219
loEvent.logEvent('google_docs_save', event);
177-
} catch(err) {
220+
} catch (err) {
178221
/*
179222
Oddball events, like text selections.
180223
*/
181224
event = {
182-
'doc_id': googledocs_id_from_url(request.url),
225+
'doc_id': googledocs_id_from_url(request.url),
183226
'url': request.url,
184227
'formdata': formdata,
185228
'rev': formdata.rev,
186229
'timestamp': parseInt(request.timeStamp, 10)
187230
};
188231
loEvent.logEvent('google_docs_save_extra', event);
189232
}
190-
} else if(this_a_google_docs_bind(request)) {
233+
} else if (this_a_google_docs_bind(request)) {
191234
logFromServiceWorker(request);
192235
} else {
193-
logFromServiceWorker("Not a save or bind: "+request.url);
236+
logFromServiceWorker("Not a save or bind: " + request.url);
194237
}
195238
},
196239
{ urls: ["*://docs.google.com/*"] },
@@ -202,10 +245,10 @@ chrome.webRequest.onBeforeRequest.addListener(
202245
chrome.runtime.onInstalled.addListener(reinjectContentScripts);
203246
async function reinjectContentScripts() {
204247
for (const contentScript of chrome.runtime.getManifest().content_scripts) {
205-
for (const tab of await chrome.tabs.query({url: contentScript.matches})) {
248+
for (const tab of await chrome.tabs.query({ url: contentScript.matches })) {
206249
// re-inject content script
207250
await chrome.scripting.executeScript({
208-
target: {tabId: tab.id, allFrames: true},
251+
target: { tabId: tab.id, allFrames: true },
209252
files: contentScript.js,
210253
}, function () {
211254
if (!chrome.runtime.lastError) {
@@ -215,10 +258,3 @@ async function reinjectContentScripts() {
215258
}
216259
}
217260
}
218-
219-
// Let the server know we've loaded.
220-
loEvent.logEvent("extension_loaded", {});
221-
222-
// And let the console know we've loaded
223-
// chrome.extension.getBackgroundPage().console.log("Loaded"); remove
224-
logFromServiceWorker("Loaded");

extension/writing-process/src/manifest.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"storage",
1818
"nativeMessaging",
1919
"scripting",
20-
"activeTab"
20+
"activeTab",
21+
"tabs"
2122
],
2223
"icons": {
2324
"48": "assets/lousy-fountain-pen-48.png"

extension/writing-process/src/writing.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,19 @@ import { googledocs_id_from_url, treeget } from './writing_common';
1010
General Utility Functions
1111
*/
1212

13+
// Notify the service worker that this content script is active. We also
14+
// provide a hook to let the service worker know when the script is about to
15+
// unload so it can perform any necessary cleanup (for example, shutting down
16+
// loggers when no tabs are active).
17+
if (chrome.runtime?.id !== undefined) {
18+
chrome.runtime.sendMessage({ type: 'content_script_ready' });
19+
window.addEventListener('beforeunload', () => {
20+
if (chrome.runtime?.id !== undefined) {
21+
chrome.runtime.sendMessage({ type: 'content_script_unloading' });
22+
}
23+
});
24+
}
25+
1326
function log_error(error_string) {
1427
/*
1528
We should send errors to the server, but for now, we

learning_observer/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.0+2025.08.07T20.03.33.270Z.c17a54d7.berickson.202507.new.lti.updates
1+
0.1.0+2025.09.15T18.58.19.025Z.cb6381ff.berickson.202509.extension.fixes

learning_observer/learning_observer/incoming_student_event.py

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,14 @@ async def handle_incoming_client_event(metadata):
187187
# The adapter allows us to handle old event formats
188188
adapter = learning_observer.adapters.adapter.EventAdapter()
189189

190+
handler_log_closed = False
191+
192+
def close_handler_log():
193+
nonlocal handler_log_closed
194+
if not handler_log_closed:
195+
log_event.close_logfile(filename)
196+
handler_log_closed = True
197+
190198
async def handler(request, client_event):
191199
'''
192200
This is the handler for incoming client events.
@@ -206,11 +214,16 @@ async def handler(request, client_event):
206214
log_event.log_event(
207215
json.dumps(event, sort_keys=True),
208216
filename, preencoded=True, timestamp=True)
217+
if client_event.get("event") == "terminate":
218+
debug_log("Terminate event received; closing handler log file")
219+
close_handler_log()
220+
return []
209221
await pipeline(event)
210222

211223
# when the handler garbage collected (no more events are being passed through),
212224
# close the log file associated with this connection
213-
weakref.finalize(handler, log_event.close_logfile, filename)
225+
weakref.finalize(handler, close_handler_log)
226+
handler.close = close_handler_log
214227

215228
return handler
216229

@@ -291,20 +304,32 @@ def decode_and_log_event(msg):
291304
)
292305
COUNT += 1
293306

307+
decoder_log_closed = False
308+
309+
def close_decoder_logfile():
310+
nonlocal decoder_log_closed
311+
if not decoder_log_closed:
312+
log_event.close_logfile(filename)
313+
decoder_log_closed = True
314+
294315
async def decode_and_log_event(events):
295316
'''
296317
Take an aiohttp web sockets message, log it, and return
297318
a clean event.
298319
'''
299-
async for msg in events:
300-
if isinstance(msg, dict):
301-
json_event = msg
302-
else:
303-
json_event = json.loads(msg.data)
304-
log_event.log_event(json_event, filename=filename)
305-
yield json_event
306-
# done processing events, can close logfile now
307-
log_event.close_logfile(filename)
320+
try:
321+
async for msg in events:
322+
if isinstance(msg, dict):
323+
json_event = msg
324+
else:
325+
json_event = json.loads(msg.data)
326+
log_event.log_event(json_event, filename=filename)
327+
yield json_event
328+
finally:
329+
# done processing events, can close logfile now
330+
close_decoder_logfile()
331+
332+
decode_and_log_event.close = close_decoder_logfile
308333
return decode_and_log_event
309334

310335

@@ -434,6 +459,21 @@ async def decode_lock_fields(events):
434459
event.update(lock_fields)
435460
yield event
436461

462+
async def handle_terminate_events(events):
463+
'''Stop processing when a terminate event is received.'''
464+
async for event in events:
465+
if event.get('event') == 'terminate':
466+
debug_log('Terminate event received; shutting down connection and cleaning up logs.')
467+
handler_close = getattr(event_handler, 'close', None)
468+
if callable(handler_close):
469+
handler_close()
470+
decoder_close = getattr(decoder_and_logger, 'close', None)
471+
if callable(decoder_close):
472+
decoder_close()
473+
await ws.close()
474+
return
475+
yield event
476+
437477
async def filter_blacklist_events(events):
438478
'''This function stops the event pipeline if sources
439479
should be blocked.
@@ -498,6 +538,7 @@ async def process_ws_message_through_pipeline():
498538
events = process_message_from_ws()
499539
events = decoder_and_logger(events)
500540
events = decode_lock_fields(events)
541+
events = handle_terminate_events(events)
501542
events = handle_auth_events(events)
502543
events = filter_blacklist_events(events)
503544
events = process_blob_storage_events(events)

0 commit comments

Comments
 (0)