Skip to content
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.0+2026.02.02T16.19.01.249Z.2ae8ac1a.berickson.20260120.comm.protocol.targets
0.1.0+2026.02.02T20.54.51.168Z.c0f2a8f0.berickson.20260113.extension.tab.ids
5 changes: 3 additions & 2 deletions extension/writing-process/src/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ Background script. This works across all of Google Chrome.
*/

import { CONFIG } from "./service_worker_config.js";
import { googledocs_id_from_url } from './writing_common';

import { googledocs_id_from_url, googledocs_tab_id_from_url } from './writing_common';
import * as loEvent from 'lo_event/lo_event/lo_event.js';
import * as loEventDebug from 'lo_event/lo_event/debugLog.js';
import { websocketLogger } from 'lo_event/lo_event/websocketLogger.js';
Expand Down Expand Up @@ -203,6 +202,7 @@ chrome.webRequest.onBeforeRequest.addListener(
versus GMT. */
event = {
'doc_id': googledocs_id_from_url(request.url),
'tab_id': googledocs_tab_id_from_url(request.url),
'url': request.url,
'bundles': JSON.parse(formdata.bundles),
'rev': formdata.rev,
Expand All @@ -216,6 +216,7 @@ chrome.webRequest.onBeforeRequest.addListener(
*/
event = {
'doc_id': googledocs_id_from_url(request.url),
'tab_id': googledocs_tab_id_from_url(request.url),
'url': request.url,
'formdata': formdata,
'rev': formdata.rev,
Expand Down
15 changes: 14 additions & 1 deletion extension/writing-process/src/writing.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
/* For debugging purposes: we know the extension is active */
// document.body.style.border = "1px solid blue";

import { googledocs_id_from_url, treeget } from './writing_common';
import { googledocs_id_from_url, googledocs_tab_id_from_url, treeget } from './writing_common';
/*
General Utility Functions
*/
Expand Down Expand Up @@ -49,6 +49,7 @@ function log_event(event_type, event) {
"type": "http://schema.learning-observer.org/writing-observer/",
"title": google_docs_title(),
"id": doc_id(),
"tab_id": tab_id(),
"url": window.location.href,
};

Expand Down Expand Up @@ -78,6 +79,18 @@ function doc_id() {
}
}

function tab_id() {
/*
Extract the Google document's current Tab ID from the window
*/
try {
return googledocs_tab_id_from_url(window.location.href);
} catch(error) {
log_error("Couldn't read document's tab id");
return null;
}
}


function this_is_a_google_doc() {
/*
Expand Down
24 changes: 24 additions & 0 deletions extension/writing-process/src/writing_common.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,30 @@ export function googledocs_id_from_url(url) {
return null;
}

export function googledocs_tab_id_from_url(url) {
/*
Given a URL like:
https://docs.google.com/document/d/<doc_id>/edit?tab=t.95yb7msfl8ul
https://docs.google.com/document/d/<doc_id>/edit?tab=t.95yb7msfl8ul#heading=h.abc123
extract the associated tab ID:
t.95yb7msfl8ul
Return null if not a valid Google Docs URL or tab param.

Regex explanation:
1. `/.*:\/\/` - match any protocol (http/https) followed by ://
2. `docs\.google\.com\/document\/` - match google docs domain
3. `.*` - match any characters until we find the tab param
4. `[?&]tab=` - match tab parameter in query string
5. `([^&#]+)` - capture tab value, stopping at & (next param) or # (hash fragment)
6. `/i` - case insensitive
*/
var match = url.match(/.*:\/\/docs\.google\.com\/document\/.*[?&]tab=([^&#]+)/i);
if (match) {
return match[1];
}
return null;
}

var writing_lasthash = "";
function unique_id() {
/*
Expand Down
2 changes: 1 addition & 1 deletion modules/writing_observer/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.0+2026.01.13T18.33.25.519Z.0984e08f.berickson.20260113.abstract.time.on.task.reducers
0.1.0+2026.02.02T20.54.51.168Z.c0f2a8f0.berickson.20260113.extension.tab.ids
14 changes: 13 additions & 1 deletion modules/writing_observer/writing_observer/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,13 @@
{
'context': "org.mitros.writing_analytics",
'scope': writing_observer.writing_analysis.gdoc_scope,
'function': writing_observer.writing_analysis.time_on_task,
'function': writing_observer.writing_analysis.gdoc_scope_time_on_task,
'default': {'saved_ts': 0}
},
{
'context': "org.mitros.writing_analytics",
'scope': writing_observer.writing_analysis.gdoc_tab_scope,
'function': writing_observer.writing_analysis.gdoc_tab_scope_time_on_task,
'default': {'saved_ts': 0}
},
{
Expand All @@ -286,6 +292,12 @@
'function': writing_observer.writing_analysis.document_list,
'default': {'docs': []}
},
{
'context': "org.mitros.writing_analytics",
'scope': writing_observer.writing_analysis.gdoc_scope,
'function': writing_observer.writing_analysis.tab_list,
'default': {'tabs': {}}
},
{
'context': "org.mitros.writing_analytics",
'scope': writing_observer.writing_analysis.student_scope,
Expand Down
61 changes: 55 additions & 6 deletions modules/writing_observer/writing_observer/reconstruct_doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def __new__(cls):
new_object._text = ""
new_object._position = 0
new_object._edit_metadata = {}
new_object._tabs = {}
new_object.fix_validity()
return new_object

Expand Down Expand Up @@ -100,6 +101,14 @@ def from_json(json_rep):
new_object._text = json_rep.get('text', '')
new_object._position = json_rep.get('position', 0)
new_object._edit_metadata = json_rep.get('edit_metadata', {})

if 'tabs' in json_rep and json_rep['tabs']:
new_object._tabs = {}
for tab_id, tab_data in json_rep['tabs'].items():
new_object._tabs[tab_id] = google_text.from_json(tab_data)
else:
new_object._tabs = {}

new_object.fix_validity()
return new_object

Expand Down Expand Up @@ -155,11 +164,14 @@ def json(self):
'''
This serializes to JSON.
'''
return {
result = {
'text': self._text,
'position': self._position,
'edit_metadata': self._edit_metadata
}
if self._tabs:
result['tabs'] = {tab_id: tab.json for tab_id, tab in self._tabs.items()}
return result


def get_parsed_text(self):
Expand All @@ -169,18 +181,23 @@ def get_parsed_text(self):
return self._text.replace(PLACEHOLDER, "")


def dispatch_command(doc, cmd):
if cmd['ty'] in dispatch:
doc = dispatch[cmd['ty']](doc, **cmd)
else:
print("Unrecogized Google Docs command: " + repr(cmd['ty']))
# TODO: Log issue and fix it!
return doc


def command_list(doc, commands):
'''
This will process a list of commands. It is helpful either when
loading the history of a new doc, or in updating a document from
new `save` requests.
'''
for item in commands:
if item['ty'] in dispatch:
doc = dispatch[item['ty']](doc, **item)
else:
print("Unrecogized Google Docs command: " + repr(item['ty']))
# TODO: Log issue and fix it!
doc = dispatch_command(doc, item)
return doc


Expand Down Expand Up @@ -301,6 +318,34 @@ def null(doc, **kwargs):
return doc


def nm(doc, nmc, nmr, **kwargs):
'''
Handle named commands for tabs (sub-documents).

* `nmc` is the command to execute
* `nmr` is the name/reference list, which contains the target tab ID
'''
# Find the target tab from the nmr list
target_tab = None
for item in reversed(nmr or []):
if isinstance(item, str) and item.startswith("t."):
target_tab = item
break

if target_tab is None:
# No tab specified, apply to main document
doc = dispatch_command(doc, nmc)
else:
# Ensure the tab exists
if target_tab not in doc._tabs:
doc._tabs[target_tab] = google_text()

# Apply the command to the sub-document
doc._tabs[target_tab] = dispatch_command(doc._tabs[target_tab], nmc)

return doc


# This dictionary maps the `ty` parameter to the function which
# handles data of that type.

Expand All @@ -312,6 +357,7 @@ def null(doc, **kwargs):
# these can't be handled like plain 'is' or 'ds' because the include different fields
# (e.g., 'sugid', presumably, suggestion id.)
dispatch = {
'ac': null, # new tab title
'ae': null,
'ase': null, # suggestion
'ast': null, # suggestion. Image?
Expand All @@ -326,8 +372,10 @@ def null(doc, **kwargs):
'is': insert,
'iss': null, # suggested insertion
'mefd': null, # suggestion
'mkch': null, # name of the first tab
'mlti': multi,
'msfd': null, # suggestion
'nm': nm, # named command for tabs
'null': null,
'ord': null,
'ras': null, # suggestion. Autospell?
Expand All @@ -344,6 +392,7 @@ def null(doc, **kwargs):
'sl': null,
'ste': null, # suggestion
'sue': null, # suggestion
'ucp': null, # updated tab title
'uefd': null, # suggestion
'use': null, # suggestion
'umv': null,
Expand Down
Loading
Loading