avi-analytics-sdk
Version:
An analytics SDK for capturing user interactions
177 lines (176 loc) • 7.64 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import axios from "axios";
import * as pako from "pako";
import { EVENT_SEND_INTERVAL, EVENTS_ENDPOINT, OBSERVABILITY_ENDPOINT, RECORDING_ENDPOINT, } from "../config/constants";
let captureEventCallback;
const eventQueue = [];
let initialized = false;
// Helper to batch and send events
function flushEvents(sessionId) {
if (eventQueue.length === 0)
return;
// Prepare the batch payload
const batchPayload = eventQueue.map((event) => ({
session_id: sessionId,
event_type: event.type,
method: event.method || null,
url: event.url || null,
status: event.status || null,
headers: event.headers ? JSON.stringify(event.headers) : null,
body: event.body ? JSON.stringify(event.body) : null,
timestamp: event.timestamp,
}));
// Clear the queue before sending
eventQueue.length = 0;
// Compress the payload
const compressedPayload = pako.deflate(JSON.stringify(batchPayload));
// Send the batch
axios
.post(OBSERVABILITY_ENDPOINT, compressedPayload, {
headers: {
"Content-Encoding": "deflate",
"Content-Type": "application/octet-stream",
},
})
.catch((err) => {
console.error("Failed to send observability events:", err);
});
}
function setupNetworkInterception(excludedUrls) {
// Intercept fetch
const originalFetch = window.fetch;
window.fetch = (...args) => __awaiter(this, void 0, void 0, function* () {
var _a;
const [url, options] = args;
// Exclude calls made to excluded URLs
if (excludedUrls.some((excludedUrl) => url.toString().includes(excludedUrl))) {
return originalFetch(...args);
}
// Log request
captureEventCallback === null || captureEventCallback === void 0 ? void 0 : captureEventCallback({
type: "request",
method: (_a = options === null || options === void 0 ? void 0 : options.method) !== null && _a !== void 0 ? _a : "GET",
url: url.toString(),
headers: (options === null || options === void 0 ? void 0 : options.headers) || {},
payload: options === null || options === void 0 ? void 0 : options.body,
timestamp: new Date().toISOString(),
});
try {
const response = yield originalFetch(...args);
const clonedResponse = response.clone();
let responseBody;
try {
responseBody = yield clonedResponse.text();
}
catch (e) {
responseBody = null;
}
// Log response
const headerEntries = [...response.headers.entries()].reduce((acc, [key, value]) => {
acc[key] = value;
return acc;
}, {});
captureEventCallback === null || captureEventCallback === void 0 ? void 0 : captureEventCallback({
type: "response",
url: url.toString(),
status: response.status,
headers: JSON.stringify(headerEntries),
body: responseBody,
timestamp: new Date().toISOString(),
});
return response;
}
catch (error) {
// Log error
captureEventCallback === null || captureEventCallback === void 0 ? void 0 : captureEventCallback({
type: "error",
url: url.toString(),
message: error instanceof Error ? error.message : "Unknown error",
timestamp: new Date().toISOString(),
});
throw error;
}
});
// Intercept XMLHttpRequest
const originalXHR = window.XMLHttpRequest;
class CustomXHR extends XMLHttpRequest {
constructor() {
super();
this.requestMethod = "";
this.requestUrl = "";
this.setupEventListeners();
}
setupEventListeners() {
this.addEventListener("loadstart", () => {
if (!excludedUrls.some((excludedUrl) => this.requestUrl.includes(excludedUrl))) {
captureEventCallback === null || captureEventCallback === void 0 ? void 0 : captureEventCallback({
type: "request",
method: this.requestMethod || "GET",
url: this.requestUrl,
payload: this._requestData,
timestamp: new Date().toISOString(),
});
}
});
this.addEventListener("loadend", () => {
if (!excludedUrls.some((excludedUrl) => this.requestUrl.includes(excludedUrl))) {
captureEventCallback === null || captureEventCallback === void 0 ? void 0 : captureEventCallback({
type: "response",
url: this.requestUrl,
status: this.status,
headers: this.getAllResponseHeaders(),
body: this.responseText,
timestamp: new Date().toISOString(),
});
}
});
this.addEventListener("error", () => {
if (!excludedUrls.some((excludedUrl) => this.requestUrl.includes(excludedUrl))) {
captureEventCallback === null || captureEventCallback === void 0 ? void 0 : captureEventCallback({
type: "error",
url: this.requestUrl,
timestamp: new Date().toISOString(),
});
}
});
}
open(method, url, async = true, username, password) {
this.requestMethod = method;
this.requestUrl = url.toString();
super.open(method, url, async, username || null, password || null);
}
send(data) {
this._requestData = data;
super.send(data);
}
}
window.XMLHttpRequest = CustomXHR;
}
export function initializeObservability({ apiKey, sessionId, excludedUrls = [OBSERVABILITY_ENDPOINT, EVENTS_ENDPOINT, RECORDING_ENDPOINT], // Default to exclude OBSERVABILITY_ENDPOINT itself
}) {
if (!apiKey || !sessionId) {
throw new Error("API key, session ID, and API endpoint are required to initialize observability.");
}
if (initialized) {
console.warn("Observability is already initialized.");
return;
}
initialized = true;
// Set up event capturing callback
captureEventCallback = (event) => {
eventQueue.push(event);
};
// Start the interval to flush events every 5 seconds
setInterval(() => flushEvents(sessionId), EVENT_SEND_INTERVAL);
// Setup network interception
setupNetworkInterception(excludedUrls);
console.log("Observability initialized with API Key:", apiKey);
}