V13-FE-011: finalize search list layout slice
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { kbxApi } from '../http/generated/kbxApiClient'
|
||||
import { kbxTelemetryCatalog, type KbxApiOperationId, type KbxTelemetryEventName, type KbxUxEvent } from '@kbx/contracts'
|
||||
|
||||
const forbiddenKeyPattern = /(phone|address|name|orderNo|customer|itemCode|barcode|keyword|query|email|businessNumber|entityId)/i
|
||||
const MAX_BUFFER = 50
|
||||
const FLUSH_INTERVAL_MS = 5000
|
||||
|
||||
function sessionId() {
|
||||
const key='kbx.telemetry.session-id'
|
||||
const current=sessionStorage.getItem(key)
|
||||
if(current) return current
|
||||
const value=crypto.randomUUID();sessionStorage.setItem(key,value);return value
|
||||
}
|
||||
|
||||
function sanitizeAttributes(eventName:KbxTelemetryEventName, attributes:Record<string,string>|undefined){
|
||||
if(!attributes) return undefined
|
||||
const allowed=new Set<string>(kbxTelemetryCatalog[eventName].allowedAttributes as readonly string[])
|
||||
const result:Record<string,string>={}
|
||||
for(const [key,value] of Object.entries(attributes)){
|
||||
if(!allowed.has(key)) throw new Error(`Telemetry attribute '${key}' is not allowed for ${eventName}.`)
|
||||
if(forbiddenKeyPattern.test(key)) throw new Error(`Telemetry attribute '${key}' may contain business/sensitive data.`)
|
||||
const normalized=String(value).slice(0,80)
|
||||
if(normalized.length) result[key]=normalized
|
||||
}
|
||||
return Object.keys(result).length?result:undefined
|
||||
}
|
||||
|
||||
class KbxTelemetryClient {
|
||||
private buffer:KbxUxEvent[]=[]
|
||||
private timer:number|undefined
|
||||
private experimentByScreen=new Map<string,{experimentId:string;experimentVariant:string}>()
|
||||
|
||||
bindExperiment(screenId:string,experimentId:string,experimentVariant:string){this.experimentByScreen.set(screenId,{experimentId,experimentVariant})}
|
||||
clearExperiment(screenId:string){this.experimentByScreen.delete(screenId)}
|
||||
|
||||
start(){if(typeof window==='undefined'||this.timer)return;this.timer=window.setInterval(()=>void this.flush(),FLUSH_INTERVAL_MS)}
|
||||
stop(){if(this.timer){window.clearInterval(this.timer);this.timer=undefined}void this.flush()}
|
||||
|
||||
track(eventName:KbxTelemetryEventName,input:Omit<KbxUxEvent,'eventName'|'sessionId'|'occurredAt'>={}){
|
||||
try {
|
||||
const def=kbxTelemetryCatalog[eventName]
|
||||
if(def.requiresDuration && input.durationMs==null) return false
|
||||
const experiment=input.screenId?this.experimentByScreen.get(input.screenId):undefined
|
||||
this.buffer.push({eventName,sessionId:sessionId(),occurredAt:new Date().toISOString(),...experiment,...input,attributes:sanitizeAttributes(eventName,input.attributes)})
|
||||
if(this.buffer.length>=MAX_BUFFER) void this.flush()
|
||||
return true
|
||||
} catch {
|
||||
// Telemetry is never allowed to fail or block a business command.
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async flush(){
|
||||
if(!this.buffer.length)return
|
||||
const events=this.buffer.splice(0,MAX_BUFFER)
|
||||
try{await kbxApi.request('common.uxTelemetry.ingest' as KbxApiOperationId,{body:{events}})}
|
||||
catch{this.buffer.unshift(...events);if(this.buffer.length>MAX_BUFFER*4)this.buffer.splice(0,this.buffer.length-MAX_BUFFER*4)}
|
||||
}
|
||||
}
|
||||
export const kbxTelemetry=new KbxTelemetryClient()
|
||||
|
||||
export function startKbxTask(screenId:string,screenVersion:string,taskType:string){
|
||||
const id=crypto.randomUUID(), started=performance.now()
|
||||
kbxTelemetry.track('task.start',{screenId,screenVersion,taskSessionId:id,attributes:{taskType}})
|
||||
return {
|
||||
id,
|
||||
complete(result='success'){kbxTelemetry.track('task.complete',{screenId,screenVersion,taskSessionId:id,durationMs:Math.round(performance.now()-started),attributes:{taskType,result}})},
|
||||
abandon(reasonCode='cancelled'){kbxTelemetry.track('task.abandon',{screenId,screenVersion,taskSessionId:id,durationMs:Math.round(performance.now()-started),attributes:{taskType,reasonCode}})},
|
||||
interaction(interactionType:string,commandId?:string){kbxTelemetry.track('interaction.execute',{screenId,screenVersion,taskSessionId:id,attributes:{interactionType,...(commandId?{commandId}:{})}})},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user