'use strict';
const BUSINESS_CONFIG={
VIEW: {
INTERSECTION_THRESHOLD: 0.5,
DWELL_THRESHOLD_MS: 500,
MAX_EVENTS_PER_PAGE: 100 
},
KEY: {
MAX_LENGTH: 100,
PATTERN: /^[a-zA-Z0-9._:-]+$/
},
PROPERTY: {
ENTRY_POINT_MAX: 64,
STEP_KEY_MAX: 64,
ERROR_CODE_MAX: 64,
FLOW_ID_SIZE: 36 
},
RESERVATION_STEPS: ['party', 'date', 'time', 'details', 'confirm'],
ERROR_CODES: ['slot_unavailable', 'validation_failed', 'network_error', 'server_rejected', 'timeout', 'session_expired', 'unknown'],
MAP_PROVIDERS: ['google_maps', 'apple_maps', 'yandex_maps', 'baidu_maps', 'other']
};
function isValidBusinessKey(key){
if(typeof key!=='string') return false;
if(key.length===0||key.length > BUSINESS_CONFIG.KEY.MAX_LENGTH) return false;
return BUSINESS_CONFIG.KEY.PATTERN.test(key);
}
function sanitizeBusinessKey(key){
if(typeof key!=='string') return null;
const sanitized=key.trim().slice(0, BUSINESS_CONFIG.KEY.MAX_LENGTH);
if(!BUSINESS_CONFIG.KEY.PATTERN.test(sanitized)) return null;
return sanitized;
}
function sanitizeEntryPoint(entryPoint){
if(typeof entryPoint!=='string') return null;
const sanitized=entryPoint.trim().slice(0, BUSINESS_CONFIG.PROPERTY.ENTRY_POINT_MAX);
if(!BUSINESS_CONFIG.KEY.PATTERN.test(sanitized)) return null;
return sanitized;
}
function sanitizeStepKey(stepKey){
if(typeof stepKey!=='string') return null;
const sanitized=stepKey.trim().slice(0, BUSINESS_CONFIG.PROPERTY.STEP_KEY_MAX);
if(!BUSINESS_CONFIG.KEY.PATTERN.test(sanitized)) return null;
return sanitized;
}
function sanitizeErrorCode(errorCode){
if(typeof errorCode!=='string') return null;
const sanitized=errorCode.trim().slice(0, BUSINESS_CONFIG.PROPERTY.ERROR_CODE_MAX);
if(!BUSINESS_CONFIG.KEY.PATTERN.test(sanitized)) return null;
return sanitized;
}
function sanitizeMediaIndex(index){
const num=parseInt(index, 10);
if(isNaN(num)||num < 0||num > 999) return null;
return num;
}
function sanitizeProvider(provider){
if(typeof provider!=='string') return null;
const sanitized=provider.trim();
if(BUSINESS_CONFIG.MAP_PROVIDERS.includes(sanitized)){
return sanitized;
}
return null;
}
function createMenuTracker(){
const state={
firedMenus: new Set(),
firedCategories: new Set(),
firedItems: new Set(),
eventCount: 0
};
return {
shouldFireMenu(menuKey){
if(!isValidBusinessKey(menuKey)) return false;
if(state.firedMenus.has(menuKey)) return false;
if(state.eventCount >=BUSINESS_CONFIG.VIEW.MAX_EVENTS_PER_PAGE) return false;
return true;
},
markMenuFired(menuKey){
state.firedMenus.add(menuKey);
state.eventCount++;
},
shouldFireCategory(menuKey, categoryKey){
if(!isValidBusinessKey(menuKey)) return false;
if(!isValidBusinessKey(categoryKey)) return false;
const combo=`${menuKey}:${categoryKey}`;
if(state.firedCategories.has(combo)) return false;
if(state.eventCount >=BUSINESS_CONFIG.VIEW.MAX_EVENTS_PER_PAGE) return false;
return true;
},
markCategoryFired(menuKey, categoryKey){
state.firedCategories.add(`${menuKey}:${categoryKey}`);
state.eventCount++;
},
shouldFireItem(menuKey, itemKey, categoryKey){
if(!isValidBusinessKey(menuKey)) return false;
if(!isValidBusinessKey(itemKey)) return false;
const combo=`${menuKey}:${categoryKey||''}:${itemKey}`;
if(state.firedItems.has(combo)) return false;
if(state.eventCount >=BUSINESS_CONFIG.VIEW.MAX_EVENTS_PER_PAGE) return false;
return true;
},
markItemFired(menuKey, itemKey, categoryKey){
state.firedItems.add(`${menuKey}:${categoryKey||''}:${itemKey}`);
state.eventCount++;
},
getEventCount(){
return state.eventCount;
},
isCapped(){
return state.eventCount >=BUSINESS_CONFIG.VIEW.MAX_EVENTS_PER_PAGE;
},
reset(){
state.firedMenus.clear();
state.firedCategories.clear();
state.firedItems.clear();
state.eventCount=0;
}};}
function createGalleryTracker(){
const state={
firedGalleries: new Set(),
firedMedia: new Set()
};
return {
shouldFireGallery(galleryKey){
if(!isValidBusinessKey(galleryKey)) return false;
if(state.firedGalleries.has(galleryKey)) return false;
return true;
},
markGalleryFired(galleryKey){
state.firedGalleries.add(galleryKey);
},
shouldFireMedia(galleryKey, mediaIndex){
if(!isValidBusinessKey(galleryKey)) return false;
if(mediaIndex===null||mediaIndex===undefined) return false;
const combo=`${galleryKey}:${mediaIndex}`;
if(state.firedMedia.has(combo)) return false;
return true;
},
markMediaFired(galleryKey, mediaIndex){
state.firedMedia.add(`${galleryKey}:${mediaIndex}`);
},
reset(){
state.firedGalleries.clear();
state.firedMedia.clear();
}};}
function classifyPhoneClick(href){
if(!href||typeof href!=='string') return null;
const trimmed=href.trim().toLowerCase();
if(trimmed.startsWith('tel:')){
return {
kind: 'phone_click',
targetKind: 'store_phone',
hasNumber: true
};}
return null;
}
function classifyMapClick(href, attributes={}){
if(attributes['data-xnwa-action']==='map'){
return {
kind: 'map_click',
provider: sanitizeProvider(attributes['data-xnwa-provider'])||'other',
entryPoint: sanitizeEntryPoint(attributes['data-xnwa-entry-point'])
};}
if(!href||typeof href!=='string') return null;
const trimmed=href.trim().toLowerCase();
const mapPatterns=[
{ pattern: /maps\.google/, provider: 'google_maps' },
{ pattern: /maps\.apple/, provider: 'apple_maps' },
{ pattern: /yandex\.ru\/maps/, provider: 'yandex_maps' },
{ pattern: /maps\.baidu/, provider: 'baidu_maps' },
{ pattern: /google\.com\/maps/, provider: 'google_maps' },
{ pattern: /goo\.gl\/maps/, provider: 'google_maps' }
];
for (const { pattern, provider } of mapPatterns){
if(pattern.test(trimmed)){
return {
kind: 'map_click',
provider: provider,
entryPoint: sanitizeEntryPoint(attributes['data-xnwa-entry-point'])
};}}
return null;
}
function shouldSuppressGenericClick(semanticKind){
const semanticKinds=[
'phone_click',
'map_click',
'reservation_view',
'reservation_start',
'reservation_step',
'reservation_complete',
'reservation_error'
];
return semanticKinds.includes(semanticKind);
}
function createReservationFunnel(){
const state={
flowId: null,
startTimestamp: null,
currentStep: null,
completedSteps: [],
isComplete: false,
isStarted: false
};
return {
start(crypto, entryPoint){
const flowId=generateFlowId(crypto);
state.flowId=flowId;
state.startTimestamp=Date.now();
state.currentStep=null;
state.completedSteps=[];
state.isComplete=false;
state.isStarted=true;
return {
flowId: flowId,
entryPoint: sanitizeEntryPoint(entryPoint)
};},
recordStep(stepKey){
if(!state.isStarted||state.isComplete) return null;
const sanitized=sanitizeStepKey(stepKey);
if(!sanitized) return null;
state.currentStep=sanitized;
if(!state.completedSteps.includes(sanitized)){
state.completedSteps.push(sanitized);
}
return {
flowId: state.flowId,
stepKey: sanitized,
stepIndex: state.completedSteps.length
};},
markComplete(){
if(!state.isStarted||state.isComplete) return null;
state.isComplete=true;
return {
flowId: state.flowId
};},
recordError(errorCode, stepKey){
if(!state.isStarted) return null;
const sanitizedError=sanitizeErrorCode(errorCode);
const sanitizedStep=stepKey ? sanitizeStepKey(stepKey):null;
if(!sanitizedError) return null;
return {
flowId: state.flowId,
stepKey: sanitizedStep,
errorCode: sanitizedError
};},
isStarted(){
return state.isStarted;
},
isComplete(){
return state.isComplete;
},
getFlowId(){
return state.flowId;
},
reset(){
state.flowId=null;
state.startTimestamp=null;
state.currentStep=null;
state.completedSteps=[];
state.isComplete=false;
state.isStarted=false;
}};}
function generateFlowId(crypto){
if(crypto&&crypto.randomUUID){
return crypto.randomUUID();
}
if(crypto&&crypto.getRandomValues){
const bytes=new Uint8Array(16);
crypto.getRandomValues(bytes);
bytes[6]=(bytes[6] & 0x0f) | 0x40;
bytes[8]=(bytes[8] & 0x3f) | 0x80;
const hex=Array.from(bytes).map(b=> b.toString(16).padStart(2, '0')).join('');
return `${hex.slice(0,8)}-${hex.slice(8,12)}-4${hex.slice(13,16)}-a${hex.slice(17,20)}-${hex.slice(20)}`;
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c)=> {
const r=Math.random() * 16 | 0;
const v=c==='x' ? r:(r & 0x3 | 0x8);
return v.toString(16);
});
}
function createBusinessQualityCounters(){
const counters={
semanticEventAccepted: 0,
semanticEventInvalid: 0,
semanticEventCapped: 0,
reservationDuplicateCompleteSuppressed: 0
};
return {
incrementAccepted(){
counters.semanticEventAccepted++;
},
incrementInvalid(){
counters.semanticEventInvalid++;
},
incrementCapped(){
counters.semanticEventCapped++;
},
incrementDuplicateComplete(){
counters.reservationDuplicateCompleteSuppressed++;
},
getCounters(){
return { ...counters };},
reset(){
Object.keys(counters).forEach(k=> counters[k]=0);
}};}
function looksLikePII(value){
if(typeof value!=='string') return false;
if(/[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/i.test(value)) return true;
if(/\+?[0-9][0-9\s\-().]{7,20}/.test(value)) return true;
return false;
}
function checkPropertiesForPII(eventName, properties){
const piiDetected=[];
for (const [key, value] of Object.entries(properties)){
if(looksLikePII(value)){
piiDetected.push(key);
}}
return piiDetected;
}
function validateBusinessEvent(eventName, properties={}){
const registeredEvents=[
'menu_view', 'menu_category_view', 'menu_item_view',
'gallery_view',
'phone_click', 'map_click',
'reservation_view', 'reservation_start', 'reservation_step',
'reservation_complete', 'reservation_error'
];
if(!registeredEvents.includes(eventName)){
return { valid: false, error: 'unknown_event_type' };}
const piiFields=checkPropertiesForPII(eventName, properties);
if(piiFields.length > 0){
return { valid: false, error: 'pii_detected', fields: piiFields };}
return { valid: true };}
function createVisibilityTracker(config={}){
const {
threshold=BUSINESS_CONFIG.VIEW.INTERSECTION_THRESHOLD,
dwellMs=BUSINESS_CONFIG.VIEW.DWELL_THRESHOLD_MS
}=config;
const observers=new Map();
return {
shouldTrigger(key, intersectionRatio, now=Date.now()){
if(intersectionRatio < threshold){
observers.delete(key);
return false;
}
let observer=observers.get(key);
if(!observer){
observer={ startTime: now, hasReported: false };
observers.set(key, observer);
return false;
}
const elapsed=now - observer.startTime;
if(elapsed >=dwellMs&&!observer.hasReported){
observer.hasReported=true;
return true;
}
return false;
},
reset(key){
observers.delete(key);
},
clear(){
observers.clear();
},
hasReported(key){
const observer=observers.get(key);
return observer ? observer.hasReported:false;
}};}
if(typeof module!=='undefined'&&module.exports){
module.exports={
BUSINESS_CONFIG,
isValidBusinessKey,
sanitizeBusinessKey,
sanitizeEntryPoint,
sanitizeStepKey,
sanitizeErrorCode,
sanitizeMediaIndex,
sanitizeProvider,
createMenuTracker,
createGalleryTracker,
classifyPhoneClick,
classifyMapClick,
shouldSuppressGenericClick,
createReservationFunnel,
generateFlowId,
createBusinessQualityCounters,
looksLikePII,
checkPropertiesForPII,
validateBusinessEvent,
createVisibilityTracker
};};