@mackenly/fathom-api
Version:
Typescript wrapper around Fathom Analytics API
675 lines (665 loc) • 20.2 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
FathomApi: () => FathomApi,
FathomApiError: () => FathomApiError,
FathomApiV1: () => FathomApiV1,
HttpClient: () => HttpClient,
aggregationParamsSchema: () => aggregationParamsSchema,
aggregationSchema: () => aggregationSchema,
clientOptionsSchema: () => clientOptionsSchema,
createEventSchema: () => createEventSchema,
createSiteSchema: () => createSiteSchema,
currentVisitorsParamsSchema: () => currentVisitorsParamsSchema,
currentVisitorsSchema: () => currentVisitorsSchema,
default: () => src_default,
paginationSchema: () => paginationSchema,
updateEventSchema: () => updateEventSchema,
updateSiteSchema: () => updateSiteSchema,
validate: () => validate
});
module.exports = __toCommonJS(src_exports);
// 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
var import_zod = require("zod");
var import_zod_validation_error = require("zod-validation-error");
var paginationSchema = import_zod.z.object({
limit: import_zod.z.number().min(1).max(100).optional(),
starting_after: import_zod.z.string().optional(),
ending_before: import_zod.z.string().optional()
}).strict();
var createSiteSchema = import_zod.z.object({
name: import_zod.z.string().min(1).max(255),
sharing: import_zod.z.enum(["none", "private", "public"]).optional(),
share_password: import_zod.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 = import_zod.z.object({
name: import_zod.z.string().min(1).max(255).optional(),
sharing: import_zod.z.enum(["none", "private", "public"]).optional(),
share_password: import_zod.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 = import_zod.z.object({
name: import_zod.z.string().min(1).max(255)
}).strict();
var updateEventSchema = import_zod.z.object({
name: import_zod.z.string().min(1).max(255)
}).strict();
var filterOperatorSchema = import_zod.z.enum(["is", "is not", "is like", "is not like"]);
var aggregationFilterSchema = import_zod.z.object({
property: import_zod.z.string(),
operator: filterOperatorSchema,
value: import_zod.z.string()
}).strict();
var aggregationParamsSchema = import_zod.z.object({
entity: import_zod.z.enum(["pageview", "event"]),
entity_id: import_zod.z.string().min(1),
aggregates: import_zod.z.string().min(1),
date_grouping: import_zod.z.enum(["hour", "day", "month", "year", "none"]).optional(),
field_grouping: import_zod.z.string().optional(),
sort_by: import_zod.z.string().optional(),
timezone: import_zod.z.string().optional(),
date_from: import_zod.z.string().optional(),
date_to: import_zod.z.string().optional(),
limit: import_zod.z.number().positive().optional(),
filters: import_zod.z.array(aggregationFilterSchema).optional()
}).strict();
var currentVisitorsParamsSchema = import_zod.z.object({
site_id: import_zod.z.string().min(1),
detailed: import_zod.z.boolean().optional()
}).strict();
var clientOptionsSchema = import_zod.z.object({
token: import_zod.z.string().min(1),
version: import_zod.z.enum(["v1"]).optional(),
baseUrl: import_zod.z.string().url().optional()
}).strict();
function validate(schema, data) {
try {
return schema.parse(data);
} catch (error) {
if (error instanceof import_zod.z.ZodError) {
const validationError = (0, import_zod_validation_error.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;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
FathomApi,
FathomApiError,
FathomApiV1,
HttpClient,
aggregationParamsSchema,
aggregationSchema,
clientOptionsSchema,
createEventSchema,
createSiteSchema,
currentVisitorsParamsSchema,
currentVisitorsSchema,
paginationSchema,
updateEventSchema,
updateSiteSchema,
validate
});