Skip to content

Instantly share code, notes, and snippets.

@ItsWendell
Last active January 30, 2023 10:36
Show Gist options
  • Save ItsWendell/45ebbb7d2ecc7e35f0a87b2f0cf62476 to your computer and use it in GitHub Desktop.
Save ItsWendell/45ebbb7d2ecc7e35f0a87b2f0cf62476 to your computer and use it in GitHub Desktop.
Segment Analytics client for Lambda with Fire & Forget support.
import { merge } from "lodash";
import { request } from "https";
import { v4 as uuidv4 } from "uuid";
export const name = "analytics-lamdba";
export const version = "0.1.2";
export type SegmentMessageTypes =
| "identify"
| "group"
| "page"
| "track"
| "screen"
| "alias";
export interface SegmentMessage {
userId?: string;
anonymousId?: string;
timestamp?: string;
context?: SegmentContext;
traits?: SegmentTraits;
[x: string]: any;
}
export interface SegmentContext {
active?: boolean;
app?: {
name?: string;
version?: string | number;
build?: string | number;
};
campaign?: Record<string, any>;
device?: Record<string, any>;
ip?: string;
library?: {
name?: string;
version?: string | number;
};
locale?: string;
location?: {
city?: string;
country?: string;
latitude?: string;
longitude?: string;
region?: string;
speed?: number | string;
};
network?: string;
os?: string;
page?: string;
referrer?: string;
screen?: string;
timezone?: string;
groupId?: string;
traits?: SegmentTraits;
userAgent?: string;
[x: string]: any;
}
export interface SegmentTraits {
avatar?: string; // URL to an avatar image for the user
birthday?: Date; // User’s birthday
company?: Record<string, any>; // Company the user represents, optionally containing?: name (a String), id (a String or Number), industry (a String), employee_count (a Number) or plan (a String)
createdAt?: Date; // Date the user’s account was first created. Segment recommends using ISO-8601 date strings.
description?: string; // Description of the user
email?: string; // Email address of a user
firstName?: string; // First name of a user
gender?: string; // Gender of a user
id?: string; // Unique ID in your database for a user
lastName?: string; // Last name of a user
name?: string; // Full name of a user. If you only pass a first and last name Segment automatically fills in the full name for you.
phone?: string; // Phone number of a user
title?: string; // Title of a user, usually related to their position at a specific company. Example?: “VP of Engineering”
username?: string; // User’s username. This should be unique to each user, like the usernames of Twitter or GitHub.
website?: string; // Website of a user
[x: string]: any;
}
export interface SegmentClientOptions {
host?: string;
enabled?: boolean;
debug?: boolean;
await?: boolean; // Should we await the actual response, or is fire and forget enough?
}
export interface SegmentUser {
userId?: string | null;
anonymousId?: string | null;
traits?: SegmentTraits;
context?: SegmentContext;
}
/**
* Segment Lambda Client
*/
class SegmentClient {
/** Segment User */
user: SegmentUser = {
anonymousId: uuidv4(),
};
/** Segment Authorization Token */
token = "";
/** Client Options */
options: SegmentClientOptions = {};
/** Base URL */
baseURL = "https://api.segment.io/v1";
/** Segment Write Key */
writeKey: string | undefined;
constructor(writeKey?: string, options?: SegmentClientOptions) {
if (!writeKey) {
console.warn(
"[Segment Analytics] No writeKey passed for segment analytics."
);
return;
}
this.setWriteKey(writeKey);
this.options = options ? options : {};
this.options.enabled = Boolean(writeKey);
}
private setWriteKey = (writeKey: string) => {
this.token = `Basic ${Buffer.from(`${writeKey}:`).toString("base64")}`;
};
identify = async (
userId?: string,
traits?: SegmentTraits,
config?: {
send?: boolean;
context?: SegmentContext;
}
) => {
// If userId's are equal, merge traits / context.
if (userId && this.user?.userId === userId) {
this.user = merge(this.user, {
traits,
context: config?.context,
});
} else {
// If not, set userId or anonymousId, traits and context.
this.user = {
userId: userId,
anonymousId: !userId ? uuidv4() : undefined,
traits,
context: config?.context,
};
}
// Only send this out when it's actually nessesary
if (config?.send) {
return await this.send("identify", {
traits,
context: config?.context,
});
}
};
track = (
event: string,
properties?: SegmentMessage,
context?: SegmentContext
) => {
return this.send("track", {
event,
properties,
context,
});
};
post = (type: SegmentMessageTypes, data: SegmentMessage) => {
const content = JSON.stringify(data);
const options = {
host: "api.segment.io",
path: `/v1/${type}`,
method: "POST",
headers: {
Authorization: `${this.token}`,
"Content-Type": `application/json`,
"Content-Length": Buffer.byteLength(content),
"User-Agent": `${name}/${version}`,
},
};
return new Promise((resolve, reject) => {
const req = request(
options,
this.options.await
? () => {
resolve(true);
}
: undefined
);
req.on("error", (e) => {
reject(e);
});
req.write(content);
if (!this.options.await) {
req.end(() => {
resolve(true);
});
}
});
};
send = async (type: SegmentMessageTypes, data: SegmentMessage) => {
const message: SegmentMessage = data;
message.anonymousId = !data?.userId
? message?.anonymousId || this.user.anonymousId || undefined
: undefined;
message.userId = message?.userId || this.user.userId || undefined;
message.context = message.context || {};
if (type !== "identify") {
message.context = merge(message.context, {
...(this?.user?.context || {}),
traits: this.user?.traits,
});
} else {
message.context = merge(message.context, {
...(this?.user?.context || {}),
});
}
if (!message.timestamp) {
message.timestamp = new Date().toISOString();
}
if (this.options?.enabled) {
try {
await this.post(type, message);
} catch (e) {
console.error("[Segment Analytics]", e);
return null;
}
}
return null;
};
}
export const analytics = new SegmentClient(
process.env.SEGMENT_WRITE_KEY || "",
{
debug: process.env.SEGMENT_DEBUG === "true",
}
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment