Skip to content

Secure Telemetry Engine Against Device/Network Variance - #4

Closed
hashexplaindata wants to merge 1 commit into
masterfrom
fix/vulnerability-audit-14949949574972649731
Closed

hashexplaindata wants to merge 1 commit into
masterfrom
fix/vulnerability-audit-14949949574972649731

Conversation

@hashexplaindata

Copy link
Copy Markdown
Owner

Patched multiple vulnerabilities in the RM-2 Behavioral Conformity Telemetry Engine to ensure the integrity of experimental data collection in mass-testing environments.

  1. Chronometric Contamination: The performance.now() timer in loadNextTrial() previously initiated before the browser's painting engine fully rendered the pixels on slower devices. This was patched using a nested requestAnimationFrame structure paired with a setTimeout(0), guaranteeing physical visibility before measurement begins.
  2. DOM Geometry & CSS Straitjacket: The AI recommendation badge lacked strict rendering definitions, risking subpixel text-rendering shifts. Patched with -webkit-font-smoothing: antialiased and backface-visibility: hidden. Mobile viewports were locked down (user-scalable=0, viewport-fit=cover, overscroll-behavior-y: none) and touch callouts disabled (user-select: none) to prevent mechanical distortion of reaction times. Added pointer-events: none to the badge to prevent misclicks.
  3. State Machine & Event Debouncing: Rapid double-taps pushed multiple records to the state array. Fixed by debouncing the pointerdown listener, synchronously locking state (STATE.isTrialActive = false), and completely removing the event listener upon execution. Navigation leaks were sealed via replaceState/pushState and popstate interception. Prevented multiple batch write payloads by instantly disabling the finalize button.
  4. Network Resilience & Payload Execution: Data was vulnerable to Wi-Fi drops exactly during batch.commit(). Patched with a two-tier resilience system: instantly caching STATE.results to localStorage before attempting the network request, and enabling Firestore's native offline persistence (db.enablePersistence({ synchronizeTabs: true })) to cache the batch write locally if offline.

PR created automatically by Jules for task 14949949574972649731 started by @hashexplaindata

- Implement nested requestAnimationFrame + setTimeout(0) for millisecond-accurate RT measurement
- Enforce strict absolute positioning, hardware acceleration, and viewport constraints to prevent DOM geometry shifts
- Debounce pointerdown events and implement navigation locks to prevent state machine duplication
- Add two-tier network resilience using localStorage caching and Firebase offline persistence

Co-authored-by: hashexplaindata <221828969+hashexplaindata@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 12, 2026 19:20
@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the robustness and data integrity of the telemetry engine, particularly in challenging testing environments. It addresses critical vulnerabilities related to timing accuracy, UI rendering consistency across devices, state management during rapid user interactions, and data persistence during network outages. The changes ensure that experimental data collection remains reliable and free from external variances, providing a more stable and accurate platform for behavioral conformity studies.

Highlights

  • Chronometric Contamination: Fixed an issue where the performance.now() timer in loadNextTrial() could start before UI elements were fully rendered, ensuring accurate timing measurements by using a nested requestAnimationFrame and setTimeout(0).
  • DOM Geometry & CSS Straitjacket: Addressed rendering inconsistencies for the AI recommendation badge and mobile viewports by applying strict CSS properties (-webkit-font-smoothing, backface-visibility, pointer-events: none) and viewport meta tags (user-scalable=0, viewport-fit=cover, overscroll-behavior-y: none, user-select: none) to prevent visual distortions and accidental interactions.
  • State Machine & Event Debouncing: Implemented robust state management to prevent data corruption from rapid user interactions, including debouncing pointerdown events, immediately locking trial state, removing event listeners after use, and sealing navigation leaks with replaceState/pushState and popstate interception, and disabling the finalize button instantly.
  • Network Resilience & Payload Execution: Enhanced data persistence against network interruptions by caching STATE.results to localStorage before network requests and enabling Firestore's native offline persistence, ensuring data recovery and eventual synchronization.
Changelog
  • code/experiment.js
    • Updated navigation lock to use replaceState, pushState, and a popstate event listener for robust back-navigation prevention.
    • Implemented immediate disablement of the finalize button on click to prevent multiple batch write attempts.
    • Modified loadNextTrial to use a nested requestAnimationFrame with setTimeout(0) for precise timer initiation after UI rendering.
    • Refactored createChoiceCard to debounce pointerdown events, immediately set STATE.isTrialActive to false, and remove the event listener after selection.
    • Added localStorage backup for telemetry data before network synchronization attempts.
    • Enabled Firestore's offline persistence (db.enablePersistence) to cache batch writes during network outages.
    • Included localStorage cleanup after successful data synchronization or caching.
    • Improved critical sync failure message to inform users about local data saving and advise against closing the tab.
  • code/index.html
    • Updated the viewport meta tag to include user-scalable=0 and viewport-fit=cover for stricter mobile viewport control and to prevent zooming.
  • code/style.css
    • Added overscroll-behavior-y: none, height: 100dvh, -webkit-user-select: none, and user-select: none to the body to prevent mobile overscroll and unwanted text selection.
    • Applied pointer-events: none, -webkit-font-smoothing: antialiased, and backface-visibility: hidden to the .ai-recommendation-badge for rendering stability and to prevent interaction interference.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several patches to improve the robustness and data integrity of the behavioral telemetry engine. The changes address potential issues with timing accuracy, UI rendering consistency on mobile devices, state management race conditions, and network resilience. The patches are well-implemented and address the described vulnerabilities effectively. My review includes one high-severity suggestion for experiment.js to improve the localStorage backup mechanism, ensuring data can be more reliably recovered across user sessions.

Comment thread code/experiment.js
Comment on lines +502 to +506
try {
localStorage.setItem(`telemetry_backup_${STATE.pid}`, JSON.stringify(STATE.results));
} catch (e) {
console.warn("Local storage backup failed (Quota/Privacy mode).", e);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The localStorage backup mechanism is a great addition for network resilience. However, its effectiveness is limited because the participant_id (STATE.pid) is regenerated on every page load. If the user experiences a sync failure and then reloads the page, the new session will have a new pid, and there's no way for the application to find the backup data stored under the old pid. The comment mentions manual recovery via devtools, but even that is difficult without knowing the old pid.

To make this backup truly effective for recovery across sessions, consider persisting the pid in localStorage as well.

Example:

// At the start of the script
function getOrSetPID() {
    let pid = localStorage.getItem('telemetry_pid');
    if (!pid) {
        pid = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
        localStorage.setItem('telemetry_pid', pid);
    }
    return pid;
}

const STATE = {
    pid: getOrSetPID(),
    // ... other state properties
};

// In init(), you could then check for a backup for this persistent PID.

This would make the data recovery process much more robust, allowing for automatic or at least simplified manual recovery.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: add455e3c2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread code/experiment.js
DOM.syncStatus.innerHTML = `
<div style="text-align:center;">
<span style="color:#ff453a; display:block; margin-bottom:8px;">⚠️ Sync Failed (Network Error)</span>
<span style="font-size:0.75rem; color:var(--text-secondary);">Your data is saved locally. Please do not close this tab until reconnected.</span>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate local-backup success messaging on actual backup state

When localStorage.setItem fails (for example, quota exhaustion or private browsing restrictions), this error path still tells participants "Your data is saved locally." That message is inaccurate in exactly the failure mode where no local backup exists, so users may close the tab and lose unsynced results. Please track whether the backup write succeeded and only show this reassurance when it actually did.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the experiment UI/telemetry flow against device rendering variance, rapid multi-input, navigation backtracking, and intermittent network conditions to reduce data corruption/loss during mass testing.

Changes:

  • Adds stricter mobile viewport + CSS constraints to reduce layout/interaction variance (overscroll, selection, badge rendering/click interception).
  • Strengthens experiment state machine against double-taps and repeated submissions (trial lock + debounced pointerdown, finalize button lock, navigation interception).
  • Adds network-resilience measures for writes (localStorage backup + Firestore offline persistence enablement) and updates sync-failure messaging.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.

File Description
code/style.css Adds mobile overscroll + selection constraints; strengthens AI badge rendering stability and prevents badge click interception.
code/index.html Tightens viewport meta settings to reduce mobile zoom/viewport variance.
code/experiment.js Updates navigation lock behavior, debounces/locks trial selection + final submission, and adds local/offline persistence for batch writes with revised failure UI.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment thread code/experiment.js
Comment on lines 536 to +539
onSyncSuccess();

// Clean up localStorage if sync (or cache write) was successful
localStorage.removeItem(`telemetry_backup_${STATE.pid}`);

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With Firestore offline persistence enabled, batch.commit() can resolve while the write is only queued locally (not yet acknowledged by the backend). Calling onSyncSuccess() immediately (and removing the localStorage backup) can therefore present a false “synced” state and allow the user to close the tab before the data ever reaches the server. Consider waiting for backend acknowledgment (e.g., db.waitForPendingWrites()), or showing a distinct “saved locally, pending sync” state and only clearing the backup once the server confirms receipt.

Suggested change
onSyncSuccess();
// Clean up localStorage if sync (or cache write) was successful
localStorage.removeItem(`telemetry_backup_${STATE.pid}`);
// Wait until all pending writes have been acknowledged by the backend
// before treating the payload as fully synced and clearing the backup.
await db.waitForPendingWrites();
// Clean up localStorage only after confirmed backend sync
localStorage.removeItem(`telemetry_backup_${STATE.pid}`);
onSyncSuccess();

Copilot uses AI. Check for mistakes.
Comment thread code/experiment.js
Comment on lines +445 to +447
const onSelect = (e) => {
// Prevent default touch behaviors (like zooming or scrolling) that might leak through
e.preventDefault();

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

e.preventDefault() on a pointerdown handler generally won’t prevent scrolling/zooming on touch devices; those default actions are primarily controlled via the CSS touch-action property. Since the page sets touch-action: pan-y globally (index.html), consider using an appropriate touch-action value on the choice cards (e.g., manipulation/none) if the goal is to block gesture side-effects, or adjust the comment to avoid implying preventDefault() provides that guarantee.

Copilot uses AI. Check for mistakes.
Comment thread code/style.css
-webkit-user-select: none;
user-select: none;
}

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

user-select: none is applied to the entire body, which makes all text (including the Participant ID shown on screen 10) non-selectable/copyable. If participants need to copy/paste the PID, consider scoping user-select: none to only the interactive trial UI, or explicitly re-enabling selection for the PID element(s) (similar to how textarea is handled in index.html).

Suggested change
/* Allow text selection for specific elements (e.g., Participant ID, textareas) */
textarea,
#participant-id,
.pid-display,
.allow-text-select {
-webkit-user-select: text;
user-select: text;
}

Copilot uses AI. Check for mistakes.
Comment thread code/index.html
Comment on lines +7 to +8
<!-- Added strict viewport properties to prevent Safari double-tap-to-zoom and viewport scaling -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0, viewport-fit=cover">

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The viewport meta disables zoom (maximum-scale=1.0 + user-scalable=0), which can create accessibility issues for low-vision users. Also, user-scalable=0 is less standard than user-scalable=no and may not be consistently honored; consider allowing zoom (recommended) or at least using the conventional yes/no values if zoom must be restricted.

Suggested change
<!-- Added strict viewport properties to prevent Safari double-tap-to-zoom and viewport scaling -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0, viewport-fit=cover">
<!-- Adjusted viewport properties to maintain layout while allowing user zoom for accessibility -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">

Copilot uses AI. Check for mistakes.
Comment thread code/experiment.js
Comment on lines +328 to +332
window.addEventListener('popstate', (e) => {
// Force them forward again
window.history.pushState(null, document.title, window.location.href);
// Prevent any default back-navigation behavior
e.preventDefault();

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The popstate handler calls e.preventDefault(), but popstate is not cancelable in browsers, so this is a no-op and can be misleading. Also, pushing a new history entry on every popstate can bloat the history stack if the user repeatedly triggers back navigation; consider using history.forward()/history.go(1) or replaceState in the handler to avoid unbounded history growth.

Suggested change
window.addEventListener('popstate', (e) => {
// Force them forward again
window.history.pushState(null, document.title, window.location.href);
// Prevent any default back-navigation behavior
e.preventDefault();
window.addEventListener('popstate', () => {
// Force them forward again without growing the history stack
window.history.go(1);

Copilot uses AI. Check for mistakes.
Comment thread code/experiment.js
Comment on lines +502 to +551
try {
localStorage.setItem(`telemetry_backup_${STATE.pid}`, JSON.stringify(STATE.results));
} catch (e) {
console.warn("Local storage backup failed (Quota/Privacy mode).", e);
}

try {
// Check for Firebase (initialized in index.html via firebase-config.js)
if (typeof firebase !== 'undefined' && firebase.apps.length > 0) {
const db = firebase.firestore();

// Enable offline persistence immediately if not already active.
// This ensures Firestore will cache the write and synchronize later
// if the device momentarily drops connection.
try {
await db.enablePersistence({ synchronizeTabs: true });
} catch (err) {
if (err.code === 'failed-precondition') {
console.warn('Persistence: Multiple tabs open.');
} else if (err.code === 'unimplemented') {
console.warn('Persistence: Browser unsupported.');
}
}

const batch = db.batch();

STATE.results.forEach(data => {
const docRef = db.collection(CFG.COLLECTION).doc();
batch.set(docRef, data);
});

// If offline, this resolves immediately because of persistence, writing to cache.
// When connection returns, Firestore syncs it automatically.
await batch.commit();
onSyncSuccess();

// Clean up localStorage if sync (or cache write) was successful
localStorage.removeItem(`telemetry_backup_${STATE.pid}`);

} else {
console.warn("Firebase not detected. Payload logged to console:", STATE.results);
setTimeout(onSyncSuccess, 1500); // Simulate sync delay
}
} catch (error) {
console.error("Critical Sync Failure:", error);
DOM.syncStatus.innerHTML = `<span style="color:#ff453a">⚠️ Sync Failed. Error: ${error.code || 'Network'}</span>`;
// Potential fallback: Save to localStorage for later recovery
DOM.syncStatus.innerHTML = `
<div style="text-align:center;">
<span style="color:#ff453a; display:block; margin-bottom:8px;">⚠️ Sync Failed (Network Error)</span>
<span style="font-size:0.75rem; color:var(--text-secondary);">Your data is saved locally. Please do not close this tab until reconnected.</span>
</div>`;

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The failure UI always says “Your data is saved locally”, but localStorage.setItem(...) can throw (and you explicitly catch/log that case). If the backup fails (quota/private mode), this message becomes inaccurate; track whether the local backup succeeded and tailor the displayed message accordingly (or attempt an alternative persistence mechanism).

Copilot uses AI. Check for mistakes.
@hashexplaindata

Copy link
Copy Markdown
Owner Author

Closing: Fixes have already been incorporated directly into the master branch.

@google-labs-jules

Copy link
Copy Markdown
Contributor

Closing: Fixes have already been incorporated directly into the master branch.

Understood. Acknowledging that this work is now obsolete and stopping work on this task.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants