A Val Town application for managing and viewing demos with authentication.
- Authentication: Google OAuth via LastLogin
- Dashboard: User-friendly interface showing system status
- Health Monitoring: Real-time system health checks
- Modular Architecture: Clean separation of concerns
This application uses LastLogin for authentication:
- Login: Users sign in with their Google account
- Protected Routes: All routes require authentication except public endpoints
- Logout: Users can logout via
/auth/logout
(handled automatically by LastLogin) - Session Management: Automatic session validation and renewal
The following routes are accessible without authentication:
/api/health
- System health status
Routes are protected by different authentication mechanisms:
User Authentication (Google OAuth via LastLogin):
/
- Main dashboard (shows user info and system status)/api/*
- API endpoints (except health)/views/*
- View routes including/views/glimpse/:id
/glimpse/*
- Shortcut routes including/glimpse/:id
(equivalent to/views/glimpse/:id
)
Webhook Authentication (X-API-KEY header):
/tasks/*
- Webhook endpoints for external integrations (POST requests only)- GET requests to
/tasks/*
are allowed without authentication for debug endpoints
- GET requests to
├── backend/
│ ├── controllers/ # Business logic controllers
│ ├── routes/ # Route definitions and HTTP handling
│ │ ├── api/ # API endpoints
│ │ ├── glimpse/ # Glimpse routes (enhanced with React frontend)
│ │ ├── tasks/ # Task-related routes
│ │ ├── views/ # User-facing views
│ │ └── authCheck.ts # Authentication middleware
│ └── services/ # External service integrations
├── frontend/ # React frontend assets
│ ├── glimpse.html # HTML template for glimpse views
│ ├── glimpse.tsx # React entry point
│ ├── components/ # React components
│ │ ├── GlimpseView.tsx # Main glimpse display component
│ │ ├── NotionBlock.tsx # Notion block renderer
│ │ └── NotionProperty.tsx # Property display component
│ └── README.md
├── shared/ # Shared utilities and types
│ ├── types.ts # TypeScript interfaces for Notion data
│ ├── utils.ts # Shared utility functions
│ └── README.md
└── main.tsx # Application entry point with static file serving
The application follows a clean MVC architecture with proper separation of concerns:
- Handles HTTP request/response formatting
- Extracts parameters from requests
- Applies authentication middleware
- Calls controller functions and formats responses
- Manages HTTP status codes and error responses
- Contains business logic and orchestrates service calls
- Returns plain data objects (not HTTP responses)
- Handles data validation and transformation
- Filters sensitive data (e.g., button properties)
- Provides consistent success/error response structure
- Handles external API calls (Notion, databases)
- Manages data persistence
- Returns structured results with success/error information
All controller functions return a consistent structure:
{
success: boolean,
data: any | null,
error: string | null,
details?: string // Additional error context
}
Routes then format these into appropriate HTTP responses.
The application provides multiple routes for accessing page data and user authentication:
-
GET /glimpse/login
- User-specific login redirect- Requires user authentication (Google OAuth via LastLogin)
- Looks up authenticated user's email in
GLANCE_DEMOS_DB_ID
database - If user found: Redirects to user's personal path (from Path property)
- If user not found: Creates new user record and redirects to
/glimpse/thanks
- Shows detailed error information for debugging database structure issues
-
GET /glimpse/thanks
- New user welcome page- Shows confirmation that user account was created
- Explains next steps (admin review, email with demo link)
- Provides timeline expectations (1-2 business days)
GET /views/glimpse/:id
- Get complete page data with blocks by Notion page ID (JSON only)GET /glimpse/:id
- Enhanced with React Frontend - Content negotiation based on Accept header:- Browser requests (
Accept: text/html
): Returns rich React frontend with interactive Notion content display - API requests (
Accept: application/json
): Returns raw JSON data (same as before) - Fallback: If HTML template fails to load, automatically serves JSON
- Browser requests (
The /glimpse/:id
endpoint now includes a rich React frontend when accessed via browser:
- Rich Notion Blocks: Supports headings, paragraphs, lists, code blocks, callouts, images, videos, tables, and more
- Enhanced Code Blocks: Syntax highlighting powered by Prism.js with support for 30+ languages, line numbers, and language indicators (optimized for performance with non-blocking JavaScript loading)
- Property Display: Shows all page properties with type-specific formatting and icons
- Responsive Design: Mobile-friendly layout using TailwindCSS
- Server-Side Data Injection: Initial data injected to eliminate loading states
- Error Handling: Graceful error states with retry functionality
- Loading States: Smooth loading indicators
- Navigation: Easy return to dashboard
- Debug Mode: Raw data view in development environments
- Dynamic Remote Support: Remote support section appears only when agents are assigned to the page
- Real-time Agent Updates: Fetches agent data every 5 seconds when user is authorized
- Conditional Display: Remote support section only appears when agents array has at least one item
- Complete Agent Information: Shows all blob contents including pageId, agents array, lastUpdated, and assignedAt timestamps
- Authorization-based: Only fetches agent data for authorized users (matching email addresses)
- Error Handling: Silent console logging for missing agent data without disrupting user experience
- Content Negotiation: Single endpoint serves both HTML and JSON
- Static File Serving: Frontend assets served via
/frontend/*
route - React 18.2.0: Pinned version for consistency
- TypeScript Support: Shared types for Notion data structures
- Prism.js Integration: Advanced syntax highlighting with automatic language detection, line numbers, and support for 30+ programming languages including JavaScript, TypeScript, Python, Java, C++, and more (performance-optimized with non-blocking script loading)
Note: The glimpse endpoints now provide both programmatic access (JSON) and user-friendly viewing (React frontend) from the same URL, maintaining backward compatibility while adding rich content display capabilities.
API endpoints for accessing Notion page data with different levels of detail:
GET /api/demo/:id/properties
- Returns page properties onlyGET /api/demo/:id
- Returns page properties + all blocks recursivelyGET /api/agent/:id
- Returns agent blob data for a specific page ID
Architecture:
- Routes: Handle HTTP concerns (parameter extraction, response formatting, status codes)
- Controllers: Contain business logic (
getDemoProperties
,getDemoFull
) - Services: Handle Notion API integration
Authentication Behavior:
- Browser requests: Require user authentication (Google OAuth via LastLogin)
- Internal requests: Bypass authentication when called from within the Val (identified by Deno user agent)
Response Format: Routes return the data directly from controllers on success:
{ // Notion page object with properties // For full endpoint: also includes "blocks" array with recursive block data }
On error, routes return:
{ "error": "Error message", "details": "Additional error context" }
Usage Examples:
// Internal call from within Val (no authentication needed)
const response = await fetch('/api/demo/page-id/properties');
const data = await response.json();
// External browser request (requires authentication)
// User must be logged in via Google OAuth
All glimpse routes:
- Require user authentication
- Return complete page data including properties and blocks recursively
- Filter out button properties from Notion page data
- Return standardized JSON responses (except authentication routes which redirect or show HTML)
- Use the same controller functions as the API endpoints for consistency
- Authentication: User must be authenticated via Google OAuth (handled by LastLogin)
- Database Lookup: System queries
GLANCE_DEMOS_DB_ID
database for user's email - User Creation: If not found, creates new user record with email address
- Welcome Page: Redirects to
/glimpse/thanks
with next steps information - Admin Process: Admin reviews new users and adds demo URLs manually
- User Return: User can return to
/glimpse/login
once URL is configured
The GLANCE_DEMOS_DB_ID
database must contain:
- Email property: Contains user's email address (exact match with authenticated email)
- Path property: Contains user's redirect path in format
/glimpse/:id
(optional for new users)
Supported Path property types: rich_text
, title
, url
The login endpoint provides detailed error information for debugging:
- Missing environment variables
- Database query failures
- User creation failures (falls back to access denied page)
- Invalid or missing Path properties
- Path format validation errors (must be
/glimpse/:id
)
The dashboard displays both routes in a comparison table for easy testing.
The application is built with:
- Hono: Web framework for routing and middleware
- LastLogin: Authentication service
- TypeScript: Type-safe development
- Val Town: Hosting platform
The application supports webhook endpoints for external integrations (like Notion webhooks):
Set the webhook secret in your environment:
NOTION_WEBHOOK_SECRET=your-secret-key-here
POST /tasks/notion-webhook
- Main webhook endpoint for Notion integrations (requiresX-API-KEY
header)POST /tasks/url
- Updates Notion page URL property with glimpse URL (requiresX-API-KEY
header)POST /tasks/assign
- Assigns agents to tasks based on Assigned property matching (requiresX-API-KEY
header)POST /tasks/visitor/email/link
- Sends demo link email to visitor based on Email and URL properties (requiresX-API-KEY
header)- Dynamic Host Support: Uses the request host to construct email links instead of hardcoded URLs
- Custom Domain Ready: Email links automatically match the domain used to access the webhook
- Professional Branding: Maintains consistent domain experience across all user touchpoints
POST /tasks/visitor/email/thanks
- Sends thank-you email to visitor based on Email property (requiresX-API-KEY
header)- Dynamic Host Support: Uses the request host for consistent branding
- Agent Integration: Includes agent name and email for direct follow-up communication
- Professional Follow-up: Maintains consistent domain experience for post-demo communication
POST /tasks/test
- Test endpoint for webhook authentication (requiresX-API-KEY
header)GET /tasks/debug-webhook
- Debug endpoint to check webhook configuration
The assignment webhook automatically assigns agents to tasks based on Assigned property matching:
Workflow:
- Receives webhook with page ID from Notion
- Retrieves page properties to extract Assigned and Viewing properties
- Checks if Viewing property is true - if not, skips assignment and logs result
- Queries
GLANCE_AGENTS_DB_ID
database for agents with matching Assigned property - STEP 1: Clear Current Demo Blob - Immediately clears the agent blob for this demo
- STEP 2: Find New Agents - Queries agents database by Assigned property
- STEP 3: Collect Agent Data - Fetches complete agent information and validates
- STEP 4: Clear Agents from Other Demo Blobs - Removes agents from any other demo blobs to prevent double-assignment
- STEP 5: Update Current Demo Blob - Stores new agent assignments in the current demo's blob
Blob-First Architecture:
- Immediate Updates: Agent blob is updated immediately for fast frontend response
- Multi-Blob Clearing: Ensures agents only appear in one demo blob at a time
- Eventual Consistency: Cron jobs handle Notion database cleanup in the background
- No Manual Unassignment: Assignments are cleared automatically when
Viewing = false
- Reliable: Blob operations are simpler and more reliable than complex relation management
Relation Management:
- Simplified Approach: Only updates the demo page's "Glimpse agents" property
- Automatic Bidirectional Updates: Notion automatically updates agents' "Glimpse demos" properties
- No Manual Clearing: Eliminates the risk of clearing relations without repopulating them
- Reliable Reassignment: Works correctly even when reassigning the same agents
Error Handling & Reliability:
- Transactional Approach: Collects all required data before making any changes
- Early Validation: Aborts assignment if any agent data cannot be fetched
- Atomic Updates: Critical Notion updates happen together or not at all
- Non-Blocking Blob Operations: Both blob updates and blob clearing are non-blocking
- Comprehensive Logging: Detailed checkpoint logging at each phase for debugging
- Graceful Degradation: Continues with available data when possible
- Synchronized Blob Clearing: Removes agents from affected demo blobs when reassigning
Agent Blob Storage:
- Key Pattern:
{project}--agent--{pageId}
(where project is derived from Val Town project title and pageId is from the webhook) - Data Structure:
{ "pageId": "page-id", "agents": [ { "agentId": "agent-page-id", "agentName": "Agent Name", "agentEmail": "agent@example.com", "agentMeetUrl": "https://meet.google.com/...", "agentPhone": "+1234567890" } ], "lastUpdated": "2025-09-10T19:52:00.000Z", "assignedAt": "2025-09-10T19:52:00.000Z" }
Requirements:
- Page must have a Viewing property set to true (assignment only occurs for actively viewed pages)
- Page must have an Assigned property with assigned person
- Agents database must have pages with Assigned properties matching the assigned person
- Original page must have a "Glimpse agents" relation property
- Agent pages must have a "Glimpse demos" relation property
Viewing Property Support:
- Checkbox:
true
value - Select:
"true"
option name - Rich Text:
"true"
or"yes"
text content (case-insensitive)
Response Format (Assignment Completed):
{ "success": true, "message": "Task assignment completed successfully", "pageId": "page-id", "personId": "person-id", "agentsAssigned": 2, "agentsClearedCount": 1, "agentsSkippedClearing": 1, "agentBlobUpdated": true, "blobClearsAttempted": 2, "blobClearsSuccessful": 2, "timestamp": "2025-09-10T16:51:24.733Z" }
Response Format (Assignment Skipped):
{ "success": true, "message": "Page is not being viewed - assignment skipped", "pageId": "page-id", "viewing": false, "timestamp": "2025-09-10T16:51:24.733Z" }
Relation Management:
- Smart Clearing: Only clears agents when they're assigned to different demos
- Preserves Same-Demo Assignments: No clearing when reassigning to the same demo
- Prevents Multiple Assignments: Ensures agents are only assigned to one demo at a time
- Efficient: Minimal API calls - only clears when necessary
- Reliable: Handles new agents, existing agents, and reassignments correctly
The application includes automated cleanup of stale data through dedicated cron jobs that use ALLOWED_HOSTS
for project isolation:
Purpose: Automatically clears agent assignments for pages that no longer have active agents.
Schedule: Runs every minute (configurable via Val Town cron settings)
Host Scoping: Uses ALLOWED_HOSTS
environment variable to only process agent blobs from legitimate webhook sources, preventing interference with other vals' data.
Workflow:
- Blob-First Scanning: Scans all agent blobs with key pattern
glimpse--agent--*
- Empty Agent Detection: Identifies blobs where
agents: []
(empty array) - Assignment Clearing: For each empty agent blob, clears:
- Assigned property: Removes assigned person from Notion
- Agent blob: Deletes the empty blob from storage
- Comprehensive Logging: Detailed success/failure/skip reporting for monitoring
Blob-First Architecture:
- Efficient Processing: Only processes blobs that exist, scales with actual usage
- Accurate Detection: Clears assignments based on actual agent data, not viewing status
- Storage Cleanup: Removes empty blobs to keep storage clean
- Consistent Pattern: Matches the blob-first approach used by viewing cleanup
Performance Benefits:
- Fast Execution: Blob scanning is faster than database queries
- Targeted Processing: Only processes pages with empty agent arrays
- Resource Efficient: Minimal API calls to Notion
Monitoring Output:
{ "success": true, "message": "Agent cleanup completed", "blobsProcessed": 5, "blobsSuccessful": 4, "blobsFailed": 0, "blobsSkipped": 1, "processingTime": "1.2s", "timestamp": "2025-09-10T22:15:00.000Z" }
This automated cleanup ensures that agent assignments are removed when no agents are actively assigned, maintaining clean data and preventing stale assignments from appearing in the frontend.
Purpose: Automatically cleans up stale viewing sessions that haven't received heartbeat updates.
Schedule: Runs every minute (configurable via Val Town cron settings)
Host Scoping: Uses ALLOWED_HOSTS
environment variable to only process viewing blobs from legitimate sources, ensuring project isolation and preventing interference with other vals' viewing data.
Workflow:
- Host-Scoped Scanning: Scans viewing blobs only from hosts listed in
ALLOWED_HOSTS
- Stale Session Detection: Identifies sessions where
sessionStart
is not null butlastUpdate
is older than 60 seconds - Session Cleanup: Sets
sessionStart
to null and updateslastUpdate
timestamp - Notion Sync: Updates "Session start" property to null in Notion
- Interaction Logging: Logs "Pageview ended" interactions for abandoned sessions
- Comprehensive Logging: Detailed success/failure reporting for monitoring
Security Benefits:
- Project Isolation: Only processes blobs from configured webhook/viewing hosts
- Account Safety: Prevents accidental modification of other vals' viewing sessions
- Explicit Control: Clear boundaries for which viewing data this val manages
Both cleanup crons require the ALLOWED_HOSTS
environment variable to maintain security and prevent cross-val interference in Val Town's account-level blob storage.
Webhook endpoints require the X-API-KEY
header:
curl -X POST https://your-val.web.val.run/tasks/test \ -H "X-API-KEY: your-secret-key-here"
The email service supports dynamic host detection for professional, branded email links:
- Request Host Detection: Extracts the host from incoming webhook requests (
c.req.header("host")
) - Dynamic URL Construction: Constructs email links using the request host instead of hardcoded URLs
- Fallback Support: Uses original Notion URL if no request host is available
- Custom Domain Ready: Works automatically with any custom domain setup
- Professional Branding: Email links match the domain users are familiar with
- Consistent Experience: Maintains domain consistency across all user touchpoints
- Future-Proof: No code changes needed when adding new domains
- User accesses app via
demo.company.com
- Notion webhook triggered, sent to
demo.company.com/tasks/visitor/email/link
- Handler extracts host:
"demo.company.com"
- Email service creates link:
https://demo.company.com/glimpse/abc123
- User receives professional, branded email link
- Email Service:
sendVisitorDemoLink()
accepts optionalrequestHost
parameter - Webhook Handler: Extracts host from request and passes to email service
- URL Construction:
https://${requestHost}/glimpse/${pageId}
when host provided - Sender Address: Remains static (
username.project@valtown.email
) for deliverability
Use the webhook testing form in the dashboard:
- Navigate to your dashboard at
/
- Find the "Webhook Endpoint" section
- Enter your
NOTION_WEBHOOK_SECRET
value - Click "Test Webhook" to verify authentication
Configure these environment variables for full functionality:
GLANCE_DEMOS_DB_ID
- Notion database ID for demosGLANCE_CONTENT_DB_ID
- Notion database ID for contentGLANCE_INTERACTIONS_DB_ID
- Notion database ID for interactionsGLANCE_AGENTS_DB_ID
- Notion database ID for agentsGLANCE_ENVIRONMENTS_DB_ID
- Notion database ID for environment/subdomain trackingNOTION_API_KEY
- Notion API key for database accessNOTION_WEBHOOK_SECRET
- Secret key for webhook authenticationALLOWED_SUBDOMAINS
- Comma-separated list of webhook subdomains for blob cleanup (e.g.,glance,glancedev,glancestaging
)
The application uses a project-based blob key system that derives prefixes from the Val Town project title:
- Pattern:
{project}--{type}--{id}
- Project Prefix: Extracted from Val Town project title using
parseVal()
, sanitized to first word, lowercase, alphanumeric only - Consistency: Same prefix used for both blob keys and email addresses (
username.{project}@valtown.email
) - Example: For project "Glimpse2 Runbook View", keys use
glimpse2--viewing--pageId
and emails useusername.glimpse2@valtown.email
- Multi-Project Support: Different Val Town projects can coexist without key conflicts
- Consistent Branding: Blob keys and email addresses derive from same project identifier
- Scalability: Easy to identify and manage data across different project deployments
- Clean Architecture: Simple, predictable key generation without complex fallback logic
✅ Complete - All blob operations (viewing, agent) now use project-based keys exclusively
The application includes real-time viewing analytics with immediate Notion synchronization and email-based authorization:
- Email-based Access Control: Viewing analytics are only tracked when the authenticated user's email matches the page's Email property
- Frontend Authorization: Email comparison happens on the frontend before any API calls are made
- Zero Unauthorized Calls: Users without matching emails generate no viewing API requests
- Automatic Detection: System automatically extracts emails from Notion page properties and user authentication
- Fast blob updates: Page viewing status stored in Val Town blob storage for instant response (~100ms)
- Immediate Notion sync: When users start viewing pages, Notion database is updated immediately
- Automatic cleanup: Cron job runs every minute to mark stale sessions (>1 minute old) as not viewing
- Key Pattern:
{project}--viewing--{pageId}
(where project is derived from Val Town project title and pageId is the Notion page ID) - Data Structure:
{ "pageId": "notion-page-id", "tabVisible": true, "lastUpdate": "2025-01-XX...", "userEmail": "user@example.com", "sessionId": "unique-session-id", "dateCreated": "2025-01-XX...", "sessionStart": "2025-01-XX...", "url": "https://example.com/page-path" }
- sessionStart: Single source of truth for session state and timing
null
= No active session (not viewing)timestamp
= Active session that started at this time (currently viewing)
- dateCreated: Set only when the blob is first created, never updated thereafter (first time page was ever viewed)
- lastUpdate: Updated on every viewing status change or heartbeat
- sessionId: Unique identifier preserved across updates for the same viewing session
- Session Active:
sessionStart !== null
(user is currently viewing) - Session Inactive:
sessionStart === null
(user is not viewing) - No Contradictory States: Impossible to have viewing without session start time
- Authorization Check: Frontend compares user email with page Email property
- Authorized Users: Frontend calls
/api/viewing
→ Blob updated → Interaction logged to GLANCE_INTERACTIONS_DB_ID- Session active (
sessionStart
has value): Creates interaction record with "Pageview started", session timestamp, page relation, and clean URL - Session inactive (
sessionStart
is null): No interaction logging (heartbeat only)
- Session active (
- User continues viewing: Frontend updates blob every 4 seconds (no Notion calls unless sessionStart changes)
- User leaves/session stale: Cron detects stale session → Blob updated (sessionStart cleared)
- Unauthorized Users: No API calls made, no viewing analytics tracked
The viewing system now uses an event-driven architecture with a single source of truth:
- Blob Storage: Fast response for viewing status updates
- Interactions Database: Complete audit trail of all viewing events
- Demos Database: Real-time status derived via Notion relations and rollups
Every time sessionStart
is set or cleared in the viewing blob, interaction records are automatically created:
"Pageview started" interactions:
- Name: "Pageview started"
- Date: Session start timestamp from the blob
- Glimpse demos: Relation to the viewed page
- URL: Clean URL (origin + pathname, no query parameters) where the page was viewed
- Subdomain: Subdomain where the viewing occurred
"Pageview ended" interactions:
- Name: "Pageview ended"
- Date: Session end timestamp (when session became stale)
- Glimpse demos: Relation to the viewed page
- URL: Clean URL where the page was viewed (preserved from session start)
- Subdomain: Subdomain where the viewing occurred
This provides a complete chronological record of all page viewing activities with full context including URLs for both session start and end events.
- Set when: User starts viewing a page (
sessionStart
changes fromnull
to timestamp) - Value: Uses the
sessionStart
timestamp from the blob (when the current session began) - Cleared when: Session ends (
sessionStart
becomesnull
) - Primary status indicator: Has value = viewing, empty = not viewing
- Set when: Page is viewed for the first time ever (blob
dateCreated
is set) - Value: Uses the
dateCreated
timestamp from the blob (first-ever view) - Never updated: Immutable historical record
- Purpose: Analytics on page discovery patterns
- Single Source of Truth:
sessionStart
property determines both session existence and timing - Logical Consistency:
sessionStart !== null
↔ user is viewing - No Contradictions: Impossible to have viewing state without session start time
- Simplified Logic: One property instead of separate
viewing
boolean andsessionStarted
timestamp
Pages must include an Email property in Notion for viewing analytics to work:
- Property Name: "Email" or "email"
- Property Type: Email type or Rich Text type
- Content: Must exactly match the authenticated user's email address
Pages in your Notion databases should include these properties for viewing analytics:
- Email (Email or Rich Text) - Required for authorization
- Interactions (Relation to GLANCE_INTERACTIONS_DB_ID) - Shows all viewing events
- Latest Interaction (Rollup from Interactions) - Most recent interaction timestamp
- Currently Viewing (Formula/Rollup) - Derived from recent interactions
- First Viewed (Rollup from Interactions) - When page was first discovered
The GLANCE_INTERACTIONS_DB_ID
database should include these properties:
- Name (Title) - Interaction type (e.g., "Pageview started", "Pageview ended")
- Date (Date) - When the interaction occurred
- Glimpse demos (Relation) - Links to the viewed page
- Subdomain (Rich Text) - Subdomain where the interaction occurred
- URL (URL) - Clean URL where the page was viewed (automatically populated)
- Immediate: Relation property shows linked interaction records as they're created
- Near Real-Time: Rollup properties calculate current viewing status from interaction timestamps
- Rich Context: Complete interaction history visible in the relation property
The viewing analytics system automatically captures and stores clean URLs for comprehensive tracking:
- Automatic: URLs are captured automatically when viewing sessions start
- Clean URLs: Only origin + pathname are stored (query parameters and fragments are stripped)
- Example:
https://demo.company.com/glimpse/abc123
(nothttps://demo.company.com/glimpse/abc123?ref=email&utm_source=campaign
)
- Blob Storage: URLs are stored in viewing blobs alongside other session data
- Notion Integration: URLs are automatically logged to interaction records in the
GLANCE_INTERACTIONS_DB_ID
database - Privacy-First: No sensitive query parameters or tracking data is stored
- Complete Context: Know exactly where users are viewing pages
- Multi-Domain Support: Track viewing across different domains and subdomains
- Clean Analytics: URL data without sensitive or tracking parameters
- Audit Trail: Full URL history in Notion for comprehensive reporting
- Email Authorization: Compares authenticated user email with page Email property before tracking
- Page Visibility API: Tracks when users switch tabs or minimize windows
- Periodic updates: Calls viewing API every 4 seconds while page is active (authorized users only)
- Session management: Automatic cleanup handles crashed browsers and network issues
- Performance: Non-blocking API calls don't impact user experience, zero calls for unauthorized users