Glancer addresses critical limitations in Notion's automation and logging capabilities by providing a robust external platform that integrates seamlessly with Notion databases. While Notion excels at data organization and basic automation, it lacks sophisticated error handling, detailed logging, and complex workflow orchestration. Glancer fills these gaps by moving the heavy lifting to Val Town, where we have full control over error handling, logging, and automation logic.
Notion's built-in automation has several constraints that make external processing essential:
- Limited Error Visibility: Generic error messages with no detailed logging or stack traces
- No Advanced Error Handling: Cannot implement retry logic, complex error recovery, or custom error workflows
- Performance Constraints: API rate limits and latency issues that affect user-facing experiences
- Automation Complexity: Complex workflows are difficult to implement, debug, and maintain within Notion
- No Real-time Processing: Limited ability to handle webhooks and real-time data processing
Glancer solves these by moving complex logic to Val Town where we have complete control over the execution environment, comprehensive error handling, detailed logging, and performance optimization through caching.
Task routes handle POST endpoints for Notion webhook processing, providing robust error handling and logging that Notion cannot offer:
// /routes/tasks/setAction.tsx
import { setAction } from "../../controllers/setAction.tsx";
app.post("/", async (c) => {
const { data } = await c.req.json();
try {
const page = await setAction(data);
return c.json(data);
} catch (error: any) {
console.log(error.body);
return c.json({
status: "error",
message: error.message,
}, 500);
}
});
Task routes are essential because they provide:
- Comprehensive error logging that Notion lacks
- Custom error handling and recovery logic
- Detailed request/response tracking
- Webhook validation and processing
Controllers contain business logic and Notion API interactions, while routes handle HTTP concerns. This separation keeps endpoints thin and business logic reusable:
// /controllers/setAction.tsx
import { Client } from "npm:@notionhq/client";
const notion = new Client({
auth: Deno.env.get("NOTION_API_KEY"),
});
export async function setAction(data: any) {
try {
const page = await notion.pages.create({
parent: {
database_id: Deno.env.get("GLANCE_INTERACTIONS_DB_ID"),
},
properties: {
"Name": {
title: [{ type: "text", text: { content: data.action } }],
},
"Date": {
date: { start: new Date() },
},
"Glancer demo": {
relation: [{ id: data.id }],
},
},
});
return page;
} catch (error: any) {
return {
"message": error.message,
"status": error.status,
"import.meta.url": new URL(import.meta.url).pathname,
};
}
}
This separation provides:
- Clear boundaries between HTTP handling and business logic
- Reusable controllers across different routes
- Easier testing and debugging
- Better error isolation and handling
Every Notion interaction and automation workflow is logged with detailed context, providing visibility that Notion's built-in automation cannot offer:
// Comprehensive error logging in controllers
catch (error: any) {
return {
"message": error.message,
"status": error.status,
"import.meta.url": new URL(import.meta.url).pathname,
};
}
Logging is critical because:
- Notion provides minimal error information
- Val Town's logging is accessible and searchable
- Detailed logs enable rapid debugging and issue resolution
- Error context helps prevent recurring issues
High-performance caching eliminates Notion API latency during customer-facing demonstrations:
// /crons/demoCache.tsx
import { blob } from "https://esm.town/v/std/blob?v=12";
import { Client } from "npm:@notionhq/client";
export default async function(interval: Interval) {
const pages = await notion.databases.query({
database_id: Deno.env.get("GLANCE_DEMOS_DB_ID"),
});
for (const page of pages.results) {
const blobKey = await blobKeyForDemoCache(import.meta.url, page.id);
blob.setJSON(blobKey, {
id: page.id,
cobrowse: page.properties.Cobrowse.checkbox,
});
}
}
Caching is essential because:
- Notion API calls can be slow during peak usage
- Customer-facing demos must be responsive
- Reduces API rate limit concerns
- Provides consistent performance regardless of Notion's availability
Scheduled tasks keep cached data synchronized with Notion databases:
// Cron job runs every minute to update cache
export default async function(interval: Interval) {
// Updates blob storage with latest Notion data
// Ensures demos always have current information
}
Cron jobs provide:
- Automatic cache invalidation and refresh
- Consistent data synchronization
- Reduced manual maintenance
- Reliable background processing
Secure configuration management for Notion integration and API access:
// Environment variables used throughout the application
const notion = new Client({
auth: Deno.env.get("NOTION_API_KEY"),
});
// Key databases
Deno.env.get("GLANCE_DEMOS_DB_ID") // Customer demo configurations
Deno.env.get("GLANCE_INTERACTIONS_DB_ID") // Event tracking and analytics
Required environment variables:
NOTION_API_KEY
: Notion integration token for database accessGLANCE_DEMOS_DB_ID
: Database containing customer demo configurationsGLANCE_INTERACTIONS_DB_ID
: Database for event tracking and analytics
Server-side rendered React components provide personalized experiences with durable customer references:
// /routes/views/demo/glancer.tsx
app.get("/:id", async (c) => {
const pageId = c.req.param("id");
const page = await notion.pages.retrieve({ page_id: pageId });
const props = page.properties;
const agent = {
name: props["Agent name"]?.formula?.string,
email: props["Agent email"]?.formula?.string,
phone: props["Agent phone"]?.formula?.string,
};
return c.html(
<html>
<head>
<script src="https://www.glancecdn.net/cobrowse/CobrowseJS.ashx?group=24456&site=staging"></script>
</head>
<body>
<h1>A Glance cobrowse demo for {props.Name.title[0].text.content}</h1>
{/* Personalized content based on Notion data */}
</body>
</html>
);
});
The React frontend provides:
- Unique URLs for each prospective customer
- Personalized content and agent assignments
- Durable references that persist across sessions
- Customizable demo experiences
The demo platform showcases Glance cobrowsing technology with several key capabilities:
Each prospective customer receives a unique demo page with:
- Custom URL based on their Notion database record
- Personalized agent contact information
- Tailored demo content based on preferences
- Persistent session state and tracking
JavaScript SDK integration enables real-time cobrowsing:
- Shared screen viewing between visitor and agent
- Remote assist capabilities for agent control
- Real-time interaction with embedded content
- Seamless integration with existing web applications
Dynamic PDF generation and cobrowsing:
- PDFs generated on-demand with customer-specific data
- Shared PDF viewing and annotation
- Real-time collaboration on document content
- Integration with Notion data for dynamic content
Developer-focused implementation guidance:
- Collapsible help sections with implementation tips
- Code examples and integration patterns
- Best practices for cobrowsing implementation
- Technical documentation for development teams
Comprehensive analytics for demo usage:
- Click tracking for all interactive elements
- Demo feature usage analytics
- Customer engagement metrics
- Real-time event logging to Notion database
// /utils/recordClick.tsx
export async function recordClick(action: string) {
const clickData = {
id: pageId,
"action": action, // cobrowse_start || cobrowse_request
};
fetch("/api/action", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(clickData),
});
}
Certain aspects of the application are handled by Notion rather than Val Town:
- Email notifications for demo requests
- Slack integration for agent notifications
- Automated follow-up sequences
- Alert management and escalation
- Customer information and preferences
- Agent assignments and contact details
- Demo configuration and customization
- Session scheduling and management
- Event tracking and analytics warehousing
- Customer engagement visualization
- Usage pattern analysis
- Performance metrics and reporting
- Cobrowsing agent assignments
- Availability scheduling
- Contact information management
- Performance tracking and optimization
├── routes/
│ ├── api/ # GET endpoints for JSON data exposure
│ ├── tasks/ # POST endpoints for Notion webhook processing
│ └── views/ # Browser-rendered interfaces and demos
├── controllers/ # Business logic and Notion API interactions
├── utils/ # Stateless helper functions and frontend scripts
├── crons/ # Scheduled tasks for data caching and maintenance
└── index.tsx # Main Hono application entry point
Controllers vs Utils:
- Controllers: High-level business logic, Notion API orchestration, stateful operations
- Utils: Pure functions, string manipulation, date formatting, stateless helpers
Route Organization:
/api
: GET endpoints returning JSON data for views and external services/tasks
: POST endpoints handling Notion webhooks and data processing/views
: HTML interfaces including the main cobrowsing demo platform
NOTION_API_KEY=your_notion_api_key
GLANCE_DEMOS_DB_ID=your_demo_database_id
GLANCE_INTERACTIONS_DB_ID=your_interactions_database_id
/
- Main application interface/glancer/:id
- Personalized demo pages for customers/api/*
- JSON data endpoints for frontend consumption/tasks/*
- Notion webhook handlers for automation/demo/:id
- React frontend with server-side data injection for demo pages
The /demo/:id
route demonstrates an optimized pattern for React applications that need backend data:
// Backend route with data injection using existing API endpoint
app.get("/demo/:id", async (c) => {
const id = c.req.param("id");
try {
let html = await readFile("/frontend/index.html", import.meta.url);
// Use your existing API endpoint to get the data
const apiUrl = new URL(c.req.url);
apiUrl.pathname = `/api/demo/${id}`;
const response = await fetch(apiUrl.toString());
const initialData = await response.json();
// Inject data to avoid extra round-trips
const dataScript = `<script>
window.__INITIAL_DATA__ = ${JSON.stringify(initialData)};
window.__DEMO_ID__ = ${JSON.stringify(id)};
</script>`;
html = html.replace("</head>", `${dataScript}</head>`);
return c.html(html);
} catch (error) {
// Fallback to serving HTML without initial data
return serveFile("/frontend/index.html", import.meta.url);
}
});
The React component then uses this injected data with a fallback to API calls:
// React component with data consumption (types defined inline to avoid CORS)
export function App() {
const [demoData, setDemoData] = useState<DemoData | null>(null);
useEffect(() => {
// First, try to use initial data injected by the server
if (window.__INITIAL_DATA__) {
setDemoData(window.__INITIAL_DATA__);
return;
}
// Fallback: fetch from API if no initial data
const demoId = window.__DEMO_ID__ || getDemoIdFromPath();
if (demoId && demoId !== 'demo') {
fetch(`/api/demo/${demoId}`)
.then(response => response.json())
.then(setDemoData);
}
}, []);
// Render with type-safe data handling
return (
<div>
{isNotionPage(demoData) && (
<div>
<h3>Demo Page: {demoData.properties?.Name?.title?.[0]?.plain_text}</h3>
<p>Setup Status: {demoData.properties?.["Setup status"]?.select?.name}</p>
<p>Cobrowsing: {demoData.properties?.Cobrowse?.checkbox ? 'Enabled' : 'Disabled'}</p>
</div>
)}
</div>
);
}
This pattern provides:
- Zero-latency initial render: Data is available immediately on page load
- Graceful fallback: API calls work if server-side injection fails
- Type safety: TypeScript types defined inline to avoid CORS issues
- SEO optimization: Server-rendered content with dynamic data
- Reuses existing API: Leverages your existing
/api/demo/:id
endpoint
- Framework: Hono (lightweight web framework)
- Runtime: Deno on Val Town
- Database: Notion API + Val Town blob storage for caching
- Frontend: Server-side rendered JSX with Tailwind CSS
- Scheduling: Val Town cron jobs for automated tasks
- Authentication: Custom middleware for secure access
This project prioritizes:
- Separation of Concerns: Clear boundaries between routing, business logic, and utilities
- Error Transparency: Comprehensive logging and error handling that Notion cannot provide
- Performance: Aggressive caching and optimization for customer-facing experiences
- Maintainability: Well-documented code with clear architectural patterns
- Notion Integration: Seamless bi-directional data flow with enhanced capabilities
By leveraging Val Town's capabilities, Glancer transforms Notion from a simple database into a powerful automation platform with enterprise-grade logging, error handling, performance optimization, and customer-facing demonstration capabilities.