react-native-avo-inspector
Version:
[](https://badge.fury.io/js/react-native-avo-inspector)
204 lines (203 loc) • 7.49 kB
JavaScript
/**
* This file is generated. Internal development changes should be made in the generator
* and the file should be re-generated. External contributions are welcome to submit
* changes directly to this file, and we'll apply them to the generator internally.
*/
/**
* EventSpecFetcher handles fetching event specifications from the Avo API.
*
* Endpoint: GET /trackingPlan/eventSpec
* Base URL: https://api.avo.app
*/
export class AvoEventSpecFetcher {
constructor(timeout = 2000, shouldLog = false, env, baseUrl = "https://api.avo.app") {
this.baseUrl = baseUrl;
this.timeout = timeout;
this.shouldLog = shouldLog;
this.env = env;
this.inFlightRequests = new Map();
}
/** Generates a unique key for tracking in-flight requests. */
generateRequestKey(params) {
return `${params.streamId}:${params.eventName}`;
}
/**
* Fetches an event specification from the API.
*
* Returns null if:
* - The network request fails
* - The response has an invalid status code (non-200)
* - The response is invalid or malformed
* - The request times out
*
* This method gracefully degrades - failures do not throw errors.
* When null is returned, Phase 2 should skip validation for that event.
*/
async fetch(params) {
const requestKey = this.generateRequestKey(params);
// Check if there's already an in-flight request for this spec
const existingRequest = this.inFlightRequests.get(requestKey);
if (existingRequest) {
return existingRequest;
}
// Create and track the new request
const requestPromise = this.fetchInternal(params);
this.inFlightRequests.set(requestKey, requestPromise);
try {
const result = await requestPromise;
return result;
}
finally {
// Clean up the in-flight request tracking
this.inFlightRequests.delete(requestKey);
}
}
/** Internal fetch implementation. */
async fetchInternal(params) {
if (!(this.env === "dev" || this.env === "staging")) {
return null;
}
const url = this.buildUrl(params);
try {
const wireResponse = await this.makeRequest(url);
if (!wireResponse) {
if (this.shouldLog) {
console.warn(`[Avo Inspector] Failed to fetch event spec for: ${params.eventName}`);
}
return null;
}
// Basic structure check for wire format
if (!this.hasExpectedShape(wireResponse)) {
if (this.shouldLog) {
console.warn(`[Avo Inspector] Invalid event spec response for: ${params.eventName}`);
}
return null;
}
// Parse wire format to internal format
const response = AvoEventSpecFetcher.parseEventSpecResponse(wireResponse);
return response;
}
catch (error) {
if (this.shouldLog) {
console.error(`[Avo Inspector] Error fetching event spec for: ${params.eventName}`, error);
}
return null;
}
}
/** Builds the complete URL with query parameters. */
buildUrl(params) {
const queryParams = new URLSearchParams({
apiKey: params.apiKey,
streamId: params.streamId,
eventName: params.eventName,
});
return `${this.baseUrl}/trackingPlan/eventSpec?${queryParams.toString()}`;
}
/**
* Makes an HTTP GET request using XMLHttpRequest.
* Returns the parsed JSON response or null on failure.
*/
makeRequest(url) {
return new Promise((resolve) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.timeout = this.timeout;
xhr.onload = () => {
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
resolve(response);
}
catch (error) {
if (this.shouldLog) {
console.error("[Avo Inspector] Failed to parse response:", error);
}
resolve(null);
}
}
else {
if (this.shouldLog) {
console.warn(`[Avo Inspector] Request failed with status: ${xhr.status}`);
}
resolve(null);
}
};
xhr.onerror = () => {
if (this.shouldLog) {
console.error("[Avo Inspector] Network error occurred");
}
resolve(null);
};
xhr.ontimeout = () => {
if (this.shouldLog) {
console.error(`[Avo Inspector] Request timed out after ${this.timeout}ms`);
}
resolve(null);
};
xhr.send();
});
}
/**
* Basic shape check for wire format - ensures response has the minimum expected structure.
* Uses short field names from wire format.
*/
hasExpectedShape(response) {
return (response &&
typeof response === "object" &&
Array.isArray(response.events) &&
response.metadata &&
typeof response.metadata === "object" &&
typeof response.metadata.schemaId === "string" &&
typeof response.metadata.branchId === "string" &&
typeof response.metadata.latestActionId === "string");
}
/** Parses the wire format response into internal format with meaningful field names. */
static parseEventSpecResponse(wire) {
return {
events: wire.events.map(AvoEventSpecFetcher.parseEventSpecEntry),
metadata: wire.metadata,
};
}
/** Parses a single event spec entry from wire format. */
static parseEventSpecEntry(wire) {
const props = {};
for (const entry of Object.entries(wire.p)) {
const propName = Reflect.get(entry, "0");
const propWire = Reflect.get(entry, "1");
Reflect.set(props, propName, AvoEventSpecFetcher.parsePropertyConstraints(propWire));
}
return {
branchId: wire.b,
baseEventId: wire.id,
variantIds: wire.vids,
props: props,
};
}
/** Parses property constraints from wire format. */
static parsePropertyConstraints(wire) {
const result = { type: wire.t, required: wire.r };
if (wire.l) {
result.isList = wire.l;
}
if (wire.p) {
result.pinnedValues = wire.p;
}
if (wire.v) {
result.allowedValues = wire.v;
}
if (wire.rx) {
result.regexPatterns = wire.rx;
}
if (wire.minmax) {
result.minMaxRanges = wire.minmax;
}
if (wire.children) {
result.children = {};
for (const [propName, childWire] of Object.entries(wire.children)) {
result.children[propName] =
AvoEventSpecFetcher.parsePropertyConstraints(childWire);
}
}
return result;
}
}