UNPKG

@mackenly/fathom-api

Version:

Typescript wrapper around Fathom Analytics API

634 lines (626 loc) 18.1 kB
// src/utils/http.ts var FathomApiError = class extends Error { constructor(message, status) { super(message); this.name = "FathomApiError"; this.status = status; this.error = message; } }; var HttpClient = class { constructor(options) { this.baseUrl = options.baseUrl || "https://api.usefathom.com"; this.token = options.token; this.headers = { "Authorization": `Bearer ${this.token}`, "Content-Type": "application/json", "Accept": "application/json" }; } /** * Make an HTTP request to the Fathom API * * @param method - HTTP method * @param path - API endpoint path * @param params - Query parameters * @param data - Request body for POST/PUT requests * @returns Promise with the response data * @throws FathomApiError if the API returns an error */ async request(method, path, params, data) { const url = new URL(path, this.baseUrl); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== void 0) { if (Array.isArray(value)) { if (key === "filters" && value.length > 0) { url.searchParams.append(key, JSON.stringify(value)); } else { value.forEach((item) => { url.searchParams.append(key, String(item)); }); } } else { url.searchParams.append(key, String(value)); } } }); } const requestOptions = { method, headers: this.headers, // Credentials needed for auth to work properly across different environments credentials: "same-origin" }; if (method !== "GET" && data) { requestOptions.body = JSON.stringify(data); } try { const response = await fetch(url.toString(), requestOptions); const responseData = await response.json(); if (!response.ok) { const errorResponse = responseData; if (response.status === 401) { throw new FathomApiError( "Authentication error: please check your API token", response.status ); } if (response.status === 400) { throw new FathomApiError( errorResponse.error || errorResponse.message || "Validation error", response.status ); } throw new FathomApiError( errorResponse.error || errorResponse.message || "Unknown error occurred", response.status ); } return responseData; } catch (error) { if (error instanceof FathomApiError) { throw error; } throw new FathomApiError( `Request failed: ${error instanceof Error ? error.message : String(error)}`, 500 ); } } /** * Make a GET request to the Fathom API */ async get(path, params) { return this.request("GET", path, params); } /** * Make a POST request to the Fathom API */ async post(path, data, params) { return this.request("POST", path, params, data); } /** * Make a DELETE request to the Fathom API */ async delete(path, params) { return this.request("DELETE", path, params); } }; // src/api/v1/account.ts var AccountResource = class { constructor(http) { this.http = http; } /** * Get account information * * @returns Promise with account information */ async get() { return this.http.get("/v1/account"); } }; // src/utils/validation.ts import { z } from "zod"; import { fromZodError } from "zod-validation-error"; var paginationSchema = z.object({ limit: z.number().min(1).max(100).optional(), starting_after: z.string().optional(), ending_before: z.string().optional() }).strict(); var createSiteSchema = z.object({ name: z.string().min(1).max(255), sharing: z.enum(["none", "private", "public"]).optional(), share_password: z.string().min(1).optional() }).strict().refine((data) => { if (data.sharing === "private" && !data.share_password) { return false; } return true; }, { message: "share_password is required when sharing is set to 'private'", path: ["share_password"] }); var updateSiteSchema = z.object({ name: z.string().min(1).max(255).optional(), sharing: z.enum(["none", "private", "public"]).optional(), share_password: z.string().min(1).optional() }).strict().refine((data) => { if (data.sharing === "private" && !data.share_password) { return false; } return true; }, { message: "share_password is required when sharing is set to 'private'", path: ["share_password"] }); var createEventSchema = z.object({ name: z.string().min(1).max(255) }).strict(); var updateEventSchema = z.object({ name: z.string().min(1).max(255) }).strict(); var filterOperatorSchema = z.enum(["is", "is not", "is like", "is not like"]); var aggregationFilterSchema = z.object({ property: z.string(), operator: filterOperatorSchema, value: z.string() }).strict(); var aggregationParamsSchema = z.object({ entity: z.enum(["pageview", "event"]), entity_id: z.string().min(1), aggregates: z.string().min(1), date_grouping: z.enum(["hour", "day", "month", "year", "none"]).optional(), field_grouping: z.string().optional(), sort_by: z.string().optional(), timezone: z.string().optional(), date_from: z.string().optional(), date_to: z.string().optional(), limit: z.number().positive().optional(), filters: z.array(aggregationFilterSchema).optional() }).strict(); var currentVisitorsParamsSchema = z.object({ site_id: z.string().min(1), detailed: z.boolean().optional() }).strict(); var clientOptionsSchema = z.object({ token: z.string().min(1), version: z.enum(["v1"]).optional(), baseUrl: z.string().url().optional() }).strict(); function validate(schema, data) { try { return schema.parse(data); } catch (error) { if (error instanceof z.ZodError) { const validationError = fromZodError(error); throw new FathomApiError(validationError.message, 400); } throw error; } } var aggregationSchema = aggregationParamsSchema; var currentVisitorsSchema = currentVisitorsParamsSchema; // src/api/v1/sites.ts var SitesResource = class { constructor(http) { this.http = http; } /** * List all sites * * @param params - Pagination parameters * @returns Promise with a list of sites */ async list(params) { const validatedParams = params ? validate(paginationSchema, params) : void 0; return this.http.get("/v1/sites", validatedParams); } /** * Get a single site * * @param siteId - The ID of the site * @returns Promise with site information */ async get(siteId) { if (!siteId) throw new Error("Site ID is required"); return this.http.get(`/v1/sites/${siteId}`); } /** * Create a new site * * @param params - Site creation parameters * @returns Promise with the created site */ async create(params) { const validatedParams = validate(createSiteSchema, params); return this.http.post("/v1/sites", validatedParams); } /** * Update a site * * @param siteId - The ID of the site * @param params - Site update parameters * @returns Promise with the updated site */ async update(siteId, params) { if (!siteId) throw new Error("Site ID is required"); const validatedParams = validate(updateSiteSchema, params); return this.http.post(`/v1/sites/${siteId}`, validatedParams); } /** * Wipe all data from a site * * @param siteId - The ID of the site * @returns Promise with the wiped site */ async wipe(siteId) { if (!siteId) throw new Error("Site ID is required"); return this.http.delete(`/v1/sites/${siteId}/data`); } /** * Delete a site * * @param siteId - The ID of the site * @returns Promise with the deleted site */ async delete(siteId) { if (!siteId) throw new Error("Site ID is required"); return this.http.delete(`/v1/sites/${siteId}`); } }; // src/api/v1/events.ts var EventsResource = class { constructor(http, siteId) { this.http = http; this.siteId = siteId; if (!siteId) throw new Error("Site ID is required"); } /** * List all events for a site * * @param params - Pagination parameters * @returns Promise with a list of events */ async list(params) { const validatedParams = params ? validate(paginationSchema, params) : void 0; return this.http.get(`/v1/sites/${this.siteId}/events`, validatedParams); } /** * Get a single event * * @param eventId - The ID of the event * @returns Promise with event information */ async get(eventId) { if (!eventId) throw new Error("Event ID is required"); return this.http.get(`/v1/sites/${this.siteId}/events/${eventId}`); } /** * Create a new event * * @param params - Event creation parameters * @returns Promise with the created event */ async create(params) { const validatedParams = validate(createEventSchema, params); return this.http.post(`/v1/sites/${this.siteId}/events`, validatedParams); } /** * Update an event * * @param eventId - The ID of the event * @param params - Event update parameters * @returns Promise with the updated event */ async update(eventId, params) { if (!eventId) throw new Error("Event ID is required"); const validatedParams = validate(updateEventSchema, params); return this.http.post(`/v1/sites/${this.siteId}/events/${eventId}`, validatedParams); } /** * Wipe all data from an event * * @param eventId - The ID of the event * @returns Promise with the wiped event */ async wipe(eventId) { if (!eventId) throw new Error("Event ID is required"); return this.http.delete(`/v1/sites/${this.siteId}/events/${eventId}/data`); } /** * Delete an event * * @param eventId - The ID of the event * @returns Promise with the deleted event */ async delete(eventId) { if (!eventId) throw new Error("Event ID is required"); return this.http.delete(`/v1/sites/${this.siteId}/events/${eventId}`); } }; // src/api/v1/reports.ts var ReportsResource = class { constructor(http) { this.http = http; } /** * Generate an aggregation report * * This endpoints's response does not match the API documentation. * As such, we're using the type for what actually gets returned. * This may break in the future if the API changes. * * @link https://usefathom.com/api#aggregation * @param params - Aggregation parameters * @returns Promise with aggregation data */ async aggregation(params) { const validatedParams = validate(aggregationSchema, params); return this.http.get("/v1/aggregations", validatedParams); } /** * Get current visitors for a site * * @param params - Current visitors parameters * @returns Promise with current visitors information */ async currentVisitors(params) { const validatedParams = validate(currentVisitorsSchema, params); return this.http.get("/v1/current_visitors", validatedParams); } /** * Convenience method to get pageview stats for a site * * @param siteId - The ID of the site to get pageviews for * @param options - Additional options for the aggregation * @returns Promise with pageview stats */ async getPageviewStats(siteId, options = {}) { return this.aggregation({ entity: "pageview", entity_id: siteId, aggregates: "visits,uniques,pageviews,bounce_rate,avg_duration", date_grouping: options.groupBy, timezone: options.timezone || "UTC", date_from: options.dateFrom, date_to: options.dateTo, limit: options.limit }); } /** * Convenience method to get top pages for a site * * @param siteId - The ID of the site * @param options - Additional options for the aggregation * @returns Promise with top pages data */ async getTopPages(siteId, options = {}) { return this.aggregation({ entity: "pageview", entity_id: siteId, aggregates: "visits,uniques,pageviews,bounce_rate,avg_duration", field_grouping: "pathname", sort_by: "pageviews:desc", timezone: options.timezone || "UTC", date_from: options.dateFrom, date_to: options.dateTo, limit: options.limit || 20 }); } /** * Convenience method to get referrer sources for a site * * @param siteId - The ID of the site * @param options - Additional options for the aggregation * @returns Promise with referrer data */ async getReferrerSources(siteId, options = {}) { return this.aggregation({ entity: "pageview", entity_id: siteId, aggregates: "visits,uniques", field_grouping: "referrer_hostname", sort_by: "visits:desc", timezone: options.timezone || "UTC", date_from: options.dateFrom, date_to: options.dateTo, limit: options.limit || 20 }); } /** * Convenience method to get event conversions * * @param siteId - The ID of the site * @param eventId - The ID of the event * @param options - Additional options for the aggregation * @returns Promise with event conversion data */ async getEventConversions(siteId, eventId, options = {}) { return this.aggregation({ entity: "event", entity_id: eventId, aggregates: "conversions,unique_conversions", date_grouping: options.groupBy, timezone: options.timezone || "UTC", date_from: options.dateFrom, date_to: options.dateTo }); } }; // src/api/v1/index.ts var FathomApiV1 = class { constructor(http) { this.http = http; this.account = new AccountResource(http); this.sites = new SitesResource(http); this.reports = new ReportsResource(http); } /** * Get events resource for a specific site * * @param siteId - The ID of the site * @returns Events resource */ events(siteId) { return new EventsResource(this.http, siteId); } /** * Convenience method to get account information * * @returns Promise with account information */ async getAccount() { return this.account.get(); } /** * Convenience method to get current visitors for a site * * @param siteId - The ID of the site * @param detailed - Whether to include detailed information * @returns Promise with current visitors information */ async getCurrentVisitors(siteId, detailed = false) { return this.reports.currentVisitors({ site_id: siteId, detailed }); } /** * Convenience method to get a site by ID * * @param siteId - The ID of the site * @returns Promise with site information */ async getSite(siteId) { return this.sites.get(siteId); } /** * Convenience method to get all sites * This method handles pagination automatically until all sites are retrieved * * @param limit - Maximum number of sites to retrieve (optional) * @returns Promise with an array of sites */ async getAllSites(limit) { const allSites = []; let lastId = void 0; let hasMore = true; while (hasMore) { const params = { limit: 20 }; if (lastId) { params.starting_after = lastId; } const response = await this.sites.list(params); allSites.push(...response.data); if (limit && allSites.length >= limit) { return allSites.slice(0, limit); } hasMore = response.has_more; if (hasMore && response.data.length > 0) { lastId = response.data[response.data.length - 1].id; } } return allSites; } /** * Convenience method to get an event by ID * * @param siteId - The ID of the site * @param eventId - The ID of the event * @returns Promise with event information */ async getEvent(siteId, eventId) { return this.events(siteId).get(eventId); } /** * Convenience method to get all events for a site * This method handles pagination automatically until all events are retrieved * * @param siteId - The ID of the site * @param limit - Maximum number of events to retrieve (optional) * @returns Promise with an array of events */ async getAllEvents(siteId, limit) { if (!siteId) throw new Error("Site ID is required"); const allEvents = []; let lastId = void 0; let hasMore = true; while (hasMore) { const params = { limit: 20 }; if (lastId) { params.starting_after = lastId; } const response = await this.events(siteId).list(params); allEvents.push(...response.data); if (limit && allEvents.length >= limit) { return allEvents.slice(0, limit); } hasMore = response.has_more; if (hasMore && response.data.length > 0) { lastId = response.data[response.data.length - 1].id; } } return allEvents; } }; // src/index.ts var FathomApi = class { /** * Create a new Fathom API client * * @param options - Client configuration options */ constructor(options) { const validatedOptions = validate(clientOptionsSchema, options); this.http = new HttpClient(validatedOptions); this.v1 = new FathomApiV1(this.http); } /** * Access the v1 API * * @returns Fathom Analytics API v1 client */ get api() { return this.v1; } /** * Access a specific version of the API * * @param version - API version to use * @returns API client for the specified version */ version(version) { if (version === "v1") { return this.v1; } throw new Error(`API version '${version}' is not supported`); } }; var src_default = FathomApi; export { FathomApi, FathomApiError, FathomApiV1, HttpClient, aggregationParamsSchema, aggregationSchema, clientOptionsSchema, createEventSchema, createSiteSchema, currentVisitorsParamsSchema, currentVisitorsSchema, src_default as default, paginationSchema, updateEventSchema, updateSiteSchema, validate };