@clipboard-health/analytics 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # @clipboard-health/analytics <!-- omit from toc -->
2
+
3
+ Type-safe analytics wrapper around our third-party analytics provider for user identification and event tracking.
4
+
5
+ ## Table of contents <!-- omit from toc -->
6
+
7
+ - [Install](#install)
8
+ - [Usage](#usage)
9
+ - [Basic analytics tracking](#basic-analytics-tracking)
10
+ - [Local development commands](#local-development-commands)
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install @clipboard-health/analytics
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ### Basic analytics tracking
21
+
22
+ <embedex source="packages/analytics/examples/analytics.ts">
23
+
24
+ ```ts
25
+ import { Analytics } from "@clipboard-health/analytics";
26
+
27
+ const logger = {
28
+ info: console.log,
29
+ warn: console.warn,
30
+ error: console.error,
31
+ };
32
+
33
+ {
34
+ // Basic usage with both features enabled
35
+ const analytics = new Analytics({
36
+ apiKey: "your-segment-write-key",
37
+ logger,
38
+ enabled: { identify: true, track: true },
39
+ });
40
+
41
+ // Identify a user
42
+ analytics.identify({
43
+ userId: "user-123",
44
+ traits: {
45
+ email: "user@example.com",
46
+ name: "John Doe",
47
+ createdAt: new Date("2023-01-01"),
48
+ type: "worker",
49
+ },
50
+ });
51
+
52
+ // Track an event
53
+ analytics.track({
54
+ userId: "user-123",
55
+ event: "Button Clicked",
56
+ traits: {
57
+ buttonName: "Apply",
58
+ page: "home",
59
+ plan: "worker",
60
+ },
61
+ });
62
+ }
63
+
64
+ {
65
+ // Disabled analytics example
66
+ const analytics = new Analytics({
67
+ apiKey: "your-segment-write-key",
68
+ logger,
69
+ enabled: { identify: false, track: false },
70
+ });
71
+
72
+ // These calls will be logged but not sent to Segment
73
+ analytics.identify({
74
+ userId: "user-789",
75
+ traits: { email: "test@example.com" },
76
+ });
77
+
78
+ analytics.track({
79
+ userId: "user-789",
80
+ event: "Page View",
81
+ traits: { page: "home" },
82
+ });
83
+ }
84
+ ```
85
+
86
+ </embedex>
87
+
88
+ ## Local development commands
89
+
90
+ See [`package.json`](./package.json) `scripts` for a list of commands.
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@clipboard-health/analytics",
3
+ "description": "",
4
+ "version": "0.1.0",
5
+ "bugs": "https://github.com/ClipboardHealth/core-utils/issues",
6
+ "dependencies": {
7
+ "@clipboard-health/util-ts": "3.8.0",
8
+ "@segment/analytics-node": "2.3.0",
9
+ "libphonenumber-js": "1.12.10",
10
+ "tslib": "2.8.1"
11
+ },
12
+ "devDependencies": {
13
+ "@clipboard-health/testing-core": "0.17.0"
14
+ },
15
+ "keywords": [],
16
+ "license": "MIT",
17
+ "main": "./src/index.js",
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "repository": {
22
+ "directory": "packages/analytics",
23
+ "type": "git",
24
+ "url": "git+https://github.com/ClipboardHealth/core-utils.git"
25
+ },
26
+ "type": "commonjs",
27
+ "typings": "./src/index.d.ts",
28
+ "types": "./src/index.d.ts"
29
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./lib/analytics";
package/src/index.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ tslib_1.__exportStar(require("./lib/analytics"), exports);
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../packages/analytics/src/index.ts"],"names":[],"mappings":";;;AAAA,0DAAgC"}
@@ -0,0 +1,63 @@
1
+ import { type Logger } from "@clipboard-health/util-ts";
2
+ export type UserId = string | number;
3
+ export interface CommonTraits {
4
+ createdAt?: Date;
5
+ email?: string;
6
+ name?: string;
7
+ phone?: string;
8
+ type?: string;
9
+ }
10
+ export type Traits = Record<string, unknown> & Readonly<CommonTraits>;
11
+ export interface IdentifyRequest {
12
+ userId: UserId;
13
+ traits: Traits;
14
+ }
15
+ export interface TrackRequest {
16
+ userId: UserId;
17
+ event: string;
18
+ traits: Traits;
19
+ }
20
+ export interface Enabled {
21
+ identify: boolean;
22
+ track: boolean;
23
+ }
24
+ export declare class Analytics {
25
+ private readonly enabled;
26
+ private readonly logger;
27
+ private readonly segment;
28
+ /**
29
+ * Creates a new Analytics instance.
30
+ *
31
+ * @param params.apiKey - API key for the third-party provider.
32
+ * @param params.logger - Logger instance for structured logging.
33
+ * @param params.enabled - Whether or not analytics are enabled.
34
+ */
35
+ constructor(params: {
36
+ apiKey: string;
37
+ logger: Logger;
38
+ enabled: Enabled;
39
+ });
40
+ /**
41
+ * Identifies the user in our third-party analytics provider.
42
+ *
43
+ * @param request.userId ID of the user
44
+ * @param request.traits user traits
45
+ */
46
+ identify(params: IdentifyRequest): void;
47
+ /**
48
+ * Tracks a user event in our third-party analytics provider.
49
+ *
50
+ * @param request.userId ID of the user
51
+ * @param request.event name of the event
52
+ * @param request.traits event properties
53
+ */
54
+ track(params: TrackRequest): void;
55
+ /**
56
+ * Knock and Braze update user data based on Segment Identify events. Knock requires E.164 phone
57
+ * numbers and Braze prefers them.
58
+ *
59
+ * See https://docs.knock.app/api-reference/users/update
60
+ * See https://www.braze.com/docs/user_guide/message_building_by_channel/sms_mms_rcs/user_phone_numbers
61
+ */
62
+ private normalizeTraits;
63
+ }
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Analytics = void 0;
4
+ const util_ts_1 = require("@clipboard-health/util-ts");
5
+ const analytics_node_1 = require("@segment/analytics-node");
6
+ const formatPhoneAsE164_1 = require("./formatPhoneAsE164");
7
+ class Analytics {
8
+ enabled;
9
+ logger;
10
+ segment;
11
+ /**
12
+ * Creates a new Analytics instance.
13
+ *
14
+ * @param params.apiKey - API key for the third-party provider.
15
+ * @param params.logger - Logger instance for structured logging.
16
+ * @param params.enabled - Whether or not analytics are enabled.
17
+ */
18
+ constructor(params) {
19
+ const { apiKey, logger, enabled } = params;
20
+ this.segment = new analytics_node_1.Analytics({ writeKey: apiKey });
21
+ this.logger = logger;
22
+ this.enabled = enabled;
23
+ }
24
+ /**
25
+ * Identifies the user in our third-party analytics provider.
26
+ *
27
+ * @param request.userId ID of the user
28
+ * @param request.traits user traits
29
+ */
30
+ identify(params) {
31
+ const { userId, traits } = params;
32
+ if (!this.enabled.identify) {
33
+ this.logger.info("Analytics identify is disabled, skipping", { params });
34
+ return;
35
+ }
36
+ this.segment.identify({
37
+ userId: String(userId),
38
+ traits: this.normalizeTraits(traits),
39
+ });
40
+ }
41
+ /**
42
+ * Tracks a user event in our third-party analytics provider.
43
+ *
44
+ * @param request.userId ID of the user
45
+ * @param request.event name of the event
46
+ * @param request.traits event properties
47
+ */
48
+ track(params) {
49
+ const { userId, event, traits } = params;
50
+ if (!this.enabled.track) {
51
+ this.logger.info("Analytics tracking is disabled, skipping", {
52
+ params,
53
+ });
54
+ return;
55
+ }
56
+ this.segment.track({
57
+ userId: String(userId),
58
+ event,
59
+ properties: traits,
60
+ });
61
+ }
62
+ /**
63
+ * Knock and Braze update user data based on Segment Identify events. Knock requires E.164 phone
64
+ * numbers and Braze prefers them.
65
+ *
66
+ * See https://docs.knock.app/api-reference/users/update
67
+ * See https://www.braze.com/docs/user_guide/message_building_by_channel/sms_mms_rcs/user_phone_numbers
68
+ */
69
+ normalizeTraits(traits) {
70
+ const normalized = { ...traits };
71
+ if (traits.phone && typeof traits.phone === "string") {
72
+ const result = (0, formatPhoneAsE164_1.formatPhoneAsE164)({ phone: traits.phone });
73
+ if (util_ts_1.either.isLeft(result)) {
74
+ this.logger.error(result.left.issues.map((issue) => issue.message).join(", "), {
75
+ phone: traits.phone,
76
+ });
77
+ }
78
+ else {
79
+ normalized.phone = result.right;
80
+ }
81
+ }
82
+ return normalized;
83
+ }
84
+ }
85
+ exports.Analytics = Analytics;
86
+ //# sourceMappingURL=analytics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics.js","sourceRoot":"","sources":["../../../../../packages/analytics/src/lib/analytics.ts"],"names":[],"mappings":";;;AAAA,uDAAqE;AACrE,4DAAwE;AAExE,2DAAwD;AA8BxD,MAAa,SAAS;IACH,OAAO,CAAU;IACjB,MAAM,CAAS;IACf,OAAO,CAAmB;IAE3C;;;;;;OAMG;IACH,YAAY,MAA4D;QACtE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC;QAE3C,IAAI,CAAC,OAAO,GAAG,IAAI,0BAAgB,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;QAC1D,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED;;;;;OAKG;IACI,QAAQ,CAAC,MAAuB;QACrC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;QAElC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;YAC3B,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,0CAA0C,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;YACzE,OAAO;QACT,CAAC;QAED,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC;YACpB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;YACtB,MAAM,EAAE,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC;SACrC,CAAC,CAAC;IACL,CAAC;IAED;;;;;;OAMG;IACI,KAAK,CAAC,MAAoB;QAC/B,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;QAEzC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YACxB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,0CAA0C,EAAE;gBAC3D,MAAM;aACP,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC;YACjB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;YACtB,KAAK;YACL,UAAU,EAAE,MAAM;SACnB,CAAC,CAAC;IACL,CAAC;IAED;;;;;;OAMG;IACK,eAAe,CAAC,MAAc;QACpC,MAAM,UAAU,GAAG,EAAE,GAAG,MAAM,EAAE,CAAC;QACjC,IAAI,MAAM,CAAC,KAAK,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YACrD,MAAM,MAAM,GAAG,IAAA,qCAAiB,EAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;YAE1D,IAAI,gBAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;gBACrB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;oBAC7E,KAAK,EAAE,MAAM,CAAC,KAAK;iBACpB,CAAC,CAAC;YACL,CAAC;iBAAM,CAAC;gBACN,UAAU,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;YAClC,CAAC;QACH,CAAC;QAED,OAAO,UAAU,CAAC;IACpB,CAAC;CACF;AAvFD,8BAuFC"}
@@ -0,0 +1,4 @@
1
+ import { either as E, ServiceError } from "@clipboard-health/util-ts";
2
+ export declare function formatPhoneAsE164(params: {
3
+ phone: string;
4
+ }): E.Either<ServiceError, string>;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatPhoneAsE164 = formatPhoneAsE164;
4
+ const util_ts_1 = require("@clipboard-health/util-ts");
5
+ const libphonenumber_js_1 = require("libphonenumber-js");
6
+ function formatPhoneAsE164(params) {
7
+ const { phone } = params;
8
+ try {
9
+ return util_ts_1.either.right((0, libphonenumber_js_1.parsePhoneNumberWithError)(phone, { defaultCountry: "US" }).format("E.164"));
10
+ }
11
+ catch {
12
+ return util_ts_1.either.left(new util_ts_1.ServiceError({
13
+ issues: [{ message: "Invalid phone number", code: "INVALID_PHONE_NUMBER" }],
14
+ }));
15
+ }
16
+ }
17
+ //# sourceMappingURL=formatPhoneAsE164.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatPhoneAsE164.js","sourceRoot":"","sources":["../../../../../packages/analytics/src/lib/formatPhoneAsE164.ts"],"names":[],"mappings":";;AAGA,8CAYC;AAfD,uDAAsE;AACtE,yDAA8D;AAE9D,SAAgB,iBAAiB,CAAC,MAAyB;IACzD,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,CAAC;IAEzB,IAAI,CAAC;QACH,OAAO,gBAAC,CAAC,KAAK,CAAC,IAAA,6CAAyB,EAAC,KAAK,EAAE,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IAC7F,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,gBAAC,CAAC,IAAI,CACX,IAAI,sBAAY,CAAC;YACf,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,sBAAsB,EAAE,IAAI,EAAE,sBAAsB,EAAE,CAAC;SAC5E,CAAC,CACH,CAAC;IACJ,CAAC;AACH,CAAC"}