Overview
Stay informed about important changes to your brand’s AI search visibility with OpenSight’s intelligent alert system. Get notified about visibility drops, new mentions, sentiment shifts, and new competitor appearances.
Key Features
Smart Alerts Automated detection of significant changes across 4 alert types
Webhook Integration Send alerts to your tools via custom webhooks
Email Notifications Configurable email frequency (instant, daily, weekly)
Notification Center In-app notification center with read/unread tracking
Alert Types
OpenSight monitors for four types of important events:
1. Visibility Drop Alerts
Triggered when your overall visibility score drops by more than 10%:
// From: apps/api/src/jobs/alert-checker.ts:11-41
async function checkVisibilityDrop (
brandId : string ,
currentScore : number
) : Promise <{ triggered : boolean ; message : string }> {
// Get previous snapshot
const previousSnapshot = await db
. select ()
. from ( visibilitySnapshots )
. where ( eq ( visibilitySnapshots . brandId , brandId ))
. orderBy ( desc ( visibilitySnapshots . snapshotDate ))
. limit ( 2 )
. offset ( 1 );
if ( ! previousSnapshot . length || ! previousSnapshot [ 0 ]) {
return { triggered: false , message: 'No previous snapshot' };
}
const previousScore = previousSnapshot [ 0 ]. overallScore || 0 ;
const drop = previousScore - currentScore ;
const percentDrop = ( drop / previousScore ) * 100 ;
if ( percentDrop > 10 ) {
return {
triggered: true ,
message: `Visibility dropped ${ percentDrop . toFixed ( 1 ) } % from ${ previousScore } to ${ currentScore } ` ,
};
}
return { triggered: false , message: 'No significant drop' };
}
Alert Trigger : >10% decrease in overall visibility scoreExample : Score drops from 80 to 70 (12.5% decrease) → Alert triggered
2. New Mention Alerts
Triggered when your total mention count increases:
// From: apps/api/src/jobs/alert-checker.ts:44-72
async function checkNewMentions (
brandId : string ,
currentMentions : number
) : Promise <{ triggered : boolean ; message : string }> {
const previousSnapshot = await db
. select ()
. from ( visibilitySnapshots )
. where ( eq ( visibilitySnapshots . brandId , brandId ))
. orderBy ( desc ( visibilitySnapshots . snapshotDate ))
. limit ( 2 )
. offset ( 1 );
if ( ! previousSnapshot . length || ! previousSnapshot [ 0 ]) {
return { triggered: false , message: 'No previous snapshot' };
}
const previousMentions = previousSnapshot [ 0 ]. totalMentions || 0 ;
if ( currentMentions > previousMentions ) {
return {
triggered: true ,
message: `New mentions detected: ${ currentMentions } (previously ${ previousMentions } )` ,
};
}
return { triggered: false , message: 'No new mentions' };
}
Alert Trigger : Any increase in total mentionsExample : Mentions increase from 45 to 52 → Alert triggered
3. Sentiment Shift Alerts
Triggered when sentiment changes by 5% or more:
// From: apps/api/src/jobs/alert-checker.ts:75-108
async function checkSentimentShift (
brandId : string ,
currentSentiment : { positive : number ; neutral : number ; negative : number }
) : Promise <{ triggered : boolean ; message : string }> {
const previousSnapshot = await db
. select ()
. from ( visibilitySnapshots )
. where ( eq ( visibilitySnapshots . brandId , brandId ))
. orderBy ( desc ( visibilitySnapshots . snapshotDate ))
. limit ( 2 )
. offset ( 1 );
if ( ! previousSnapshot . length || ! previousSnapshot [ 0 ]) {
return { triggered: false , message: 'No previous snapshot' };
}
const prevSent = previousSnapshot [ 0 ];
const prevPositive = parseFloat ( String ( prevSent . sentimentPositive || 0 ));
const prevNegative = parseFloat ( String ( prevSent . sentimentNegative || 0 ));
const positiveChange = currentSentiment . positive - prevPositive ;
const negativeChange = currentSentiment . negative - prevNegative ;
if ( Math . abs ( positiveChange ) >= 5 || Math . abs ( negativeChange ) >= 5 ) {
return {
triggered: true ,
message: `Sentiment shift detected: positive ${ positiveChange > 0 ? '+' : '' }${ positiveChange . toFixed ( 1 ) } %, negative ${ negativeChange > 0 ? '+' : '' }${ negativeChange . toFixed ( 1 ) } %` ,
};
}
return { triggered: false , message: 'No significant shift' };
}
Positive Shift:
Positive sentiment: 60% → 70% (+10%)
Message: “Sentiment shift detected: positive +10.0%, negative -5.0%”
Negative Shift:
Negative sentiment: 10% → 18% (+8%)
Message: “Sentiment shift detected: positive -2.0%, negative +8.0%”
Threshold : ±5% change in positive OR negative sentiment
4. New Competitor Alerts
Triggered when a competitor appears in AI results for the first time:
// From: apps/api/src/jobs/alert-checker.ts:111-146
async function checkNewCompetitors (
brandId : string ,
currentCompetitors : Record < string , unknown >
) : Promise <{ triggered : boolean ; message : string }> {
const previousSnapshot = await db
. select ()
. from ( visibilitySnapshots )
. where ( eq ( visibilitySnapshots . brandId , brandId ))
. orderBy ( desc ( visibilitySnapshots . snapshotDate ))
. limit ( 2 )
. offset ( 1 );
if ( ! previousSnapshot . length || ! previousSnapshot [ 0 ]) {
return { triggered: false , message: 'No previous snapshot' };
}
const prevCompetitors = ( previousSnapshot [ 0 ]. competitorData as Record < string , unknown >) || {};
const newCompetitors : string [] = [];
Object . keys ( currentCompetitors ). forEach (( name ) => {
if ( ! prevCompetitors [ name ]) {
newCompetitors . push ( name );
}
});
if ( newCompetitors . length > 0 ) {
return {
triggered: true ,
message: `New competitor appearances detected: ${ newCompetitors . join ( ', ' ) } ` ,
};
}
return { triggered: false , message: 'No new competitors' };
}
Alert Trigger : Competitor mentioned in current snapshot but not in previous snapshotExample : “Competitor A, Competitor B” appear in results for first time → Alert triggered
Notification Settings
Configure which alerts you want to receive and how:
Get Settings
GET /api/notifications/settings
Response:
{
"settings" : {
"userId" : "user-123" ,
"emailFrequency" : "daily" ,
"alertVisibilityDrop" : true ,
"alertNewMention" : true ,
"alertSentimentShift" : true ,
"alertCompetitorNew" : true ,
"webhookUrl" : "https://hooks.slack.com/services/..." ,
"createdAt" : "2024-01-15T10:00:00Z" ,
"updatedAt" : "2024-03-01T14:30:00Z"
}
}
Update Settings
PATCH /api/notifications/settings
Content-Type : application/json
{
"email_frequency" : "instant" ,
"alert_visibility_drop" : true ,
"alert_new_mention" : false ,
"alert_sentiment_shift" : true ,
"alert_competitor_new" : true ,
"webhook_url" : "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
}
Email Frequency Options
Receive emails immediately when alerts are triggered Best for : High-value brands requiring immediate action
Receive a daily digest of all alerts (default) Best for : Regular monitoring without inbox overload
Receive a weekly summary of alerts Best for : Lower-priority brands or overview tracking
Default Settings
When first created, notification settings default to:
// From: apps/api/src/services/notification.service.ts:90-101
const result = await db
. insert ( notificationSettings )
. values ({
userId ,
emailFrequency: 'daily' ,
alertVisibilityDrop: true ,
alertNewMention: true ,
alertSentimentShift: true ,
alertCompetitorNew: true ,
})
. returning ();
Webhook Integration
Send alerts to external tools like Slack, Discord, or custom endpoints:
Webhook Payload
// From: apps/api/src/jobs/alert-checker.ts:151-187
async function sendWebhookNotification (
webhookUrl : string ,
notification : {
type : string ;
title : string ;
body : string ;
metadata : Record < string , unknown >;
}
) : Promise < boolean > {
try {
const response = await fetch ( webhookUrl , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
},
body: JSON . stringify ({
type: notification . type ,
title: notification . title ,
body: notification . body ,
metadata: notification . metadata ,
timestamp: new Date (). toISOString (),
}),
});
if ( ! response . ok ) {
throw new Error ( `HTTP ${ response . status } ` );
}
return true ;
} catch ( error ) {
logger . error (
{ webhookUrl , error: error instanceof Error ? error . message : String ( error ) },
'Failed to send webhook notification'
);
return false ;
}
}
Webhook Example Payload
{
"type" : "visibility_drop" ,
"title" : "Visibility Drop Alert" ,
"body" : "Visibility dropped 12.5% from 80 to 70" ,
"metadata" : {
"brandId" : "brand-123" ,
"brandName" : "Acme Corp"
},
"timestamp" : "2024-03-01T14:30:00Z"
}
Slack Integration
To send alerts to Slack:
Create Incoming Webhook
Go to Slack Apps → Create Incoming Webhook for your channel
Copy Webhook URL
Copy the URL (format: https://hooks.slack.com/services/...)
Update Settings
Add the webhook URL to your notification settings: {
"webhook_url" : "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
}
Webhook failures are logged but don’t prevent in-app notifications from being created.
Notification Center
View and manage all notifications in the in-app notification center:
List Notifications
GET /api/notifications?unread=true
Response:
{
"notifications" : [
{
"id" : "notif-123" ,
"userId" : "user-123" ,
"type" : "visibility_drop" ,
"title" : "Visibility Drop Alert" ,
"body" : "Visibility dropped 12.5% from 80 to 70" ,
"metadata" : {
"brandId" : "brand-123" ,
"brandName" : "Acme Corp" ,
"severity" : "warning"
},
"readAt" : null ,
"createdAt" : "2024-03-01T14:30:00Z"
}
]
}
Filter Options
All Notifications
Unread Only
Returns all notifications (read and unread) GET /api/notifications?unread=true
Returns only unread notifications (readAt is null)
Mark as Read
PATCH /api/notifications/:id/read
Response:
{
"notification" : {
"id" : "notif-123" ,
"readAt" : "2024-03-01T15:00:00Z"
}
}
Mark All as Read
PATCH /api/notifications/read-all
Response:
Alert Checker Job
Alerts are generated by an automated job that runs after each visibility snapshot:
// From: apps/api/src/jobs/alert-checker.ts:192-350
export async function alertCheckerProcessor ( data : AlertCheckerJobData ) : Promise < any > {
const { brandId } = data ;
// 1. Get today's snapshot
const todaySnapshot = await db
. select ()
. from ( visibilitySnapshots )
. where (
and (
eq ( visibilitySnapshots . brandId , brandId ),
sql `DATE( ${ visibilitySnapshots . snapshotDate } ::date) = ${ today } ::date`
)
)
. limit ( 1 );
// 2. Get notification settings
const settings = await db
. select ()
. from ( notificationSettings )
. where ( eq ( notificationSettings . userId , brandRecord . userId ))
. limit ( 1 );
// 3. Check each alert type
const alerts : Array < Alert > = [];
if ( settings . alertVisibilityDrop ) {
const visibilityCheck = await checkVisibilityDrop ( brandId , snapshot . overallScore );
if ( visibilityCheck . triggered ) {
alerts . push ({
type: 'visibility_drop' ,
title: 'Visibility Drop Alert' ,
body: visibilityCheck . message ,
severity: 'warning' ,
});
}
}
// ... (check other alert types)
// 4. Create notifications
for ( const alert of alerts ) {
await db . insert ( notifications ). values ({
userId: brandRecord . userId ,
type: alert . type ,
title: alert . title ,
body: alert . body ,
metadata: {
brandId ,
brandName: brandRecord . name ,
severity: alert . severity ,
},
});
// 5. Send webhook if configured
if ( settings . webhookUrl ) {
await sendWebhookNotification ( settings . webhookUrl , alert );
}
}
return {
success: true ,
alertsCreated: alerts . length ,
alertTypes: alerts . map (( a ) => a . type ),
};
}
Job Configuration
// From: apps/api/src/jobs/alert-checker.ts:355-362
export async function queueAlertChecker ( data : AlertCheckerJobData ) : Promise < void > {
runJob ( 'alert-checker' , data , alertCheckerProcessor , {
attempts: 3 ,
backoffMs: 2000 ,
}). catch (( error ) => {
logger . error ({ error: error . message , brandId: data . brandId }, 'Alert checker job failed after retries' );
});
}
Job Properties:
Attempts : 3 retries on failure
Backoff : 2000ms exponential backoff
Trigger : Runs after each visibility snapshot is created
Severity Levels
Info Severity : info
New mentions
New competitor appearances
Warning Severity : warning
Visibility drops
Sentiment shifts
Error Severity : error
Critical system issues
Major visibility loss
Best Practices
Configure Alert Sensitivity
Respond to Alerts Promptly
Action Plan for Each Alert Type: Visibility Drop:
Check which AI engine(s) caused the drop
Review recent content changes
Analyze competitor activity
Update/improve affected content
New Mentions:
Read the full AI responses
Assess mention quality and sentiment
Identify what triggered the mention
Replicate success in other content
Sentiment Shift:
Identify source of negative sentiment
Address root cause (product issue, PR problem, etc.)
Create positive content to counter negative mentions
Monitor sentiment recovery
New Competitors:
Add competitor to tracking list
Analyze their content strategy
Identify gaps in your coverage
Create competitive content
Use Webhooks for Team Collaboration
Integrate with your team’s tools:
Slack : Share alerts in team channels
Discord : Notify community/support teams
Zapier : Trigger automated workflows
Custom Tools : Build internal integrations
Example Workflows:
Visibility drop → Create Jira ticket
New mention → Post to #wins channel
Sentiment shift → Alert customer success team
API Reference
Notifications
List Notifications
GET /api/notifications?unread=true
Mark as Read
PATCH /api/notifications/:id/read
Mark All as Read
PATCH /api/notifications/read-all
Settings
Get Settings
GET /api/notifications/settings
Update Settings
PATCH /api/notifications/settings
Content-Type : application/json
{
"email_frequency" : "daily" ,
"alert_visibility_drop" : true ,
"alert_new_mention" : true ,
"alert_sentiment_shift" : true ,
"alert_competitor_new" : true ,
"webhook_url" : "https://hooks.slack.com/..."
}
Next Steps
Brand Monitoring Understand what triggers alerts
Competitor Tracking Set up competitor appearance alerts
Content Scoring Prevent visibility drops with better content
AI Engines Learn which engines generate alerts