node-avo-inspector 1.1.1 → 1.2.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/.claude/scheduled_tasks.lock +1 -0
- package/.claude/settings.local.json +11 -0
- package/.claude/worktrees/release-1.1.1/LICENSE +21 -0
- package/.claude/worktrees/release-1.1.1/README.md +69 -0
- package/.claude/worktrees/release-1.1.1/examples/node-inspector-js/index.js +30 -0
- package/.claude/worktrees/release-1.1.1/examples/node-inspector-js/package.json +15 -0
- package/.claude/worktrees/release-1.1.1/examples/node-inspector-js/yarn.lock +6 -0
- package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/dist/index.js +34 -0
- package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/index.ts +30 -0
- package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/package.json +18 -0
- package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/tsconfig.json +15 -0
- package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/yarn.lock +11 -0
- package/.claude/worktrees/release-1.1.1/package-lock.json +4039 -0
- package/.claude/worktrees/release-1.1.1/package.json +48 -0
- package/.claude/worktrees/release-1.1.1/reason/AvoInspector.rei +11 -0
- package/.claude/worktrees/release-1.1.1/reason/NodeAvoInspector.re +27 -0
- package/.claude/worktrees/release-1.1.1/src/AvoDeduplicator.ts +146 -0
- package/.claude/worktrees/release-1.1.1/src/AvoEncryption.ts +90 -0
- package/.claude/worktrees/release-1.1.1/src/AvoGuid.ts +9 -0
- package/.claude/worktrees/release-1.1.1/src/AvoInspector.ts +383 -0
- package/.claude/worktrees/release-1.1.1/src/AvoInspectorEnv.ts +8 -0
- package/.claude/worktrees/release-1.1.1/src/AvoNetworkCallsHandler.ts +267 -0
- package/.claude/worktrees/release-1.1.1/src/AvoSchemaParser.ts +115 -0
- package/.claude/worktrees/release-1.1.1/src/AvoStreamId.ts +16 -0
- package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoEncryption.ts +515 -0
- package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoEventSpecCache.ts +242 -0
- package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoEventSpecFetcher.ts +241 -0
- package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoInspector.ts +360 -0
- package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoStreamId.ts +72 -0
- package/.claude/worktrees/release-1.1.1/src/__tests__/TestDeduplicator.ts +191 -0
- package/.claude/worktrees/release-1.1.1/src/__tests__/TestEventValidator.ts +231 -0
- package/.claude/worktrees/release-1.1.1/src/__tests__/TestNetworkCallsHandler.ts +179 -0
- package/.claude/worktrees/release-1.1.1/src/__tests__/TestNetworkCallsHandlerPublic.ts +208 -0
- package/.claude/worktrees/release-1.1.1/src/__tests__/TestParsing.ts +153 -0
- package/.claude/worktrees/release-1.1.1/src/__tests__/constants.ts +63 -0
- package/.claude/worktrees/release-1.1.1/src/eventSpec/AvoEventSpecCache.ts +109 -0
- package/.claude/worktrees/release-1.1.1/src/eventSpec/AvoEventSpecFetchTypes.ts +47 -0
- package/.claude/worktrees/release-1.1.1/src/eventSpec/AvoEventSpecFetcher.ts +106 -0
- package/.claude/worktrees/release-1.1.1/src/eventSpec/EventValidator.ts +74 -0
- package/.claude/worktrees/release-1.1.1/src/index.ts +2 -0
- package/.claude/worktrees/release-1.1.1/src/utils.ts +49 -0
- package/.claude/worktrees/release-1.1.1/tsconfig.json +13 -0
- package/.claude/worktrees/release-1.1.1/yarn.lock +2310 -0
- package/.github/workflows/claude.yml +55 -0
- package/dist/AvoDeduplicator.d.ts +4 -3
- package/dist/AvoDeduplicator.js +25 -24
- package/dist/AvoEncryption.d.ts +26 -0
- package/dist/AvoEncryption.js +77 -0
- package/dist/AvoInspector.d.ts +34 -3
- package/dist/AvoInspector.js +182 -36
- package/dist/AvoNetworkCallsHandler.d.ts +39 -13
- package/dist/AvoNetworkCallsHandler.js +127 -33
- package/dist/AvoStreamId.d.ts +5 -0
- package/dist/AvoStreamId.js +15 -0
- package/dist/__tests__/TestAvoEncryption.d.ts +1 -0
- package/dist/__tests__/TestAvoEncryption.js +288 -0
- package/dist/__tests__/TestAvoEventSpecCache.d.ts +1 -0
- package/dist/__tests__/TestAvoEventSpecCache.js +194 -0
- package/dist/__tests__/TestAvoEventSpecFetcher.d.ts +1 -0
- package/dist/__tests__/TestAvoEventSpecFetcher.js +219 -0
- package/dist/__tests__/TestAvoInspector.js +76 -2
- package/dist/__tests__/TestAvoStreamId.d.ts +1 -0
- package/dist/__tests__/TestAvoStreamId.js +55 -0
- package/dist/__tests__/TestEventValidator.d.ts +1 -0
- package/dist/__tests__/TestEventValidator.js +196 -0
- package/dist/__tests__/TestNetworkCallsHandler.js +37 -10
- package/dist/__tests__/TestNetworkCallsHandlerPublic.js +74 -41
- package/dist/eventSpec/AvoEventSpecCache.d.ts +19 -0
- package/dist/eventSpec/AvoEventSpecCache.js +100 -0
- package/dist/eventSpec/AvoEventSpecFetchTypes.d.ts +41 -0
- package/dist/eventSpec/AvoEventSpecFetchTypes.js +5 -0
- package/dist/eventSpec/AvoEventSpecFetcher.d.ts +25 -0
- package/dist/eventSpec/AvoEventSpecFetcher.js +159 -0
- package/dist/eventSpec/EventValidator.d.ts +15 -0
- package/dist/eventSpec/EventValidator.js +53 -0
- package/package.json +4 -2
- package/src/AvoDeduplicator.ts +31 -27
- package/src/AvoEncryption.ts +90 -0
- package/src/AvoInspector.ts +275 -49
- package/src/AvoNetworkCallsHandler.ts +205 -45
- package/src/AvoStreamId.ts +16 -0
- package/src/__tests__/TestAvoEncryption.ts +515 -0
- package/src/__tests__/TestAvoEventSpecCache.ts +242 -0
- package/src/__tests__/TestAvoEventSpecFetcher.ts +279 -0
- package/src/__tests__/TestAvoInspector.ts +114 -2
- package/src/__tests__/TestAvoStreamId.ts +72 -0
- package/src/__tests__/TestEventValidator.ts +231 -0
- package/src/__tests__/TestNetworkCallsHandler.ts +82 -15
- package/src/__tests__/TestNetworkCallsHandlerPublic.ts +110 -44
- package/src/eventSpec/AvoEventSpecCache.ts +119 -0
- package/src/eventSpec/AvoEventSpecFetchTypes.ts +47 -0
- package/src/eventSpec/AvoEventSpecFetcher.ts +186 -0
- package/src/eventSpec/EventValidator.ts +74 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const EventValidator_1 = require("../eventSpec/EventValidator");
|
|
4
|
+
// We need to mock safe-regex2 for some tests
|
|
5
|
+
jest.mock("safe-regex2", () => {
|
|
6
|
+
const actual = jest.requireActual("safe-regex2");
|
|
7
|
+
return {
|
|
8
|
+
__esModule: true,
|
|
9
|
+
default: actual.default || actual,
|
|
10
|
+
_mockable: true,
|
|
11
|
+
};
|
|
12
|
+
});
|
|
13
|
+
describe("EventValidator", () => {
|
|
14
|
+
let validator;
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
validator = new EventValidator_1.EventValidator();
|
|
17
|
+
jest.spyOn(console, "warn").mockImplementation(() => { });
|
|
18
|
+
});
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
jest.restoreAllMocks();
|
|
21
|
+
});
|
|
22
|
+
describe("type validation", () => {
|
|
23
|
+
test("passes when property types match spec", () => {
|
|
24
|
+
const spec = {
|
|
25
|
+
eventName: "click",
|
|
26
|
+
properties: [
|
|
27
|
+
{ propertyName: "target", propertyType: "string" },
|
|
28
|
+
{ propertyName: "count", propertyType: "int" },
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
const eventProperties = [
|
|
32
|
+
{ propertyName: "target", propertyType: "string" },
|
|
33
|
+
{ propertyName: "count", propertyType: "int" },
|
|
34
|
+
];
|
|
35
|
+
const results = validator.validate(spec, eventProperties, "evt-1");
|
|
36
|
+
// All properties passed type check, so failedEventIds should be empty
|
|
37
|
+
expect(results.every((r) => r.failedEventIds.length === 0)).toBe(true);
|
|
38
|
+
// Bandwidth optimization: passedEventIds cleared because passed >= failed for each property
|
|
39
|
+
expect(results.every((r) => r.passedEventIds.length === 0)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
test("fails when property type does not match spec", () => {
|
|
42
|
+
const spec = {
|
|
43
|
+
eventName: "click",
|
|
44
|
+
properties: [{ propertyName: "count", propertyType: "int" }],
|
|
45
|
+
};
|
|
46
|
+
const eventProperties = [
|
|
47
|
+
{ propertyName: "count", propertyType: "string" },
|
|
48
|
+
];
|
|
49
|
+
const results = validator.validate(spec, eventProperties, "evt-2");
|
|
50
|
+
const countResult = results.find((r) => r.propertyName === "count");
|
|
51
|
+
expect(countResult === null || countResult === void 0 ? void 0 : countResult.failedEventIds).toContain("evt-2");
|
|
52
|
+
expect(countResult === null || countResult === void 0 ? void 0 : countResult.passedEventIds).not.toContain("evt-2");
|
|
53
|
+
});
|
|
54
|
+
test("fails for missing required properties", () => {
|
|
55
|
+
const spec = {
|
|
56
|
+
eventName: "click",
|
|
57
|
+
properties: [
|
|
58
|
+
{ propertyName: "target", propertyType: "string" },
|
|
59
|
+
{ propertyName: "count", propertyType: "int" },
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
const eventProperties = [
|
|
63
|
+
{ propertyName: "target", propertyType: "string" },
|
|
64
|
+
// count is missing
|
|
65
|
+
];
|
|
66
|
+
const results = validator.validate(spec, eventProperties, "evt-3");
|
|
67
|
+
const countResult = results.find((r) => r.propertyName === "count");
|
|
68
|
+
expect(countResult === null || countResult === void 0 ? void 0 : countResult.failedEventIds).toContain("evt-3");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe("regex validation", () => {
|
|
72
|
+
test("passes when value matches safe regex pattern (type match)", () => {
|
|
73
|
+
const spec = {
|
|
74
|
+
eventName: "navigate",
|
|
75
|
+
properties: [
|
|
76
|
+
{
|
|
77
|
+
propertyName: "url",
|
|
78
|
+
propertyType: "string",
|
|
79
|
+
regex: "^https://",
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
const eventProperties = [
|
|
84
|
+
{ propertyName: "url", propertyType: "string", propertyValue: "https://example.com" },
|
|
85
|
+
];
|
|
86
|
+
const results = validator.validate(spec, eventProperties, "evt-4");
|
|
87
|
+
const urlResult = results.find((r) => r.propertyName === "url");
|
|
88
|
+
// Type matches and regex matches, property passed. Bandwidth optimization: passedEventIds cleared
|
|
89
|
+
// because passed count (1) >= failed count (0)
|
|
90
|
+
expect(urlResult === null || urlResult === void 0 ? void 0 : urlResult.failedEventIds).toEqual([]);
|
|
91
|
+
});
|
|
92
|
+
test("fails when value does not match safe regex pattern", () => {
|
|
93
|
+
const spec = {
|
|
94
|
+
eventName: "navigate",
|
|
95
|
+
properties: [
|
|
96
|
+
{
|
|
97
|
+
propertyName: "url",
|
|
98
|
+
propertyType: "string",
|
|
99
|
+
regex: "^https://",
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
const eventProperties = [
|
|
104
|
+
{ propertyName: "url", propertyType: "string", propertyValue: "http://example.com" },
|
|
105
|
+
];
|
|
106
|
+
const results = validator.validate(spec, eventProperties, "evt-regex-fail");
|
|
107
|
+
const urlResult = results.find((r) => r.propertyName === "url");
|
|
108
|
+
expect(urlResult === null || urlResult === void 0 ? void 0 : urlResult.failedEventIds).toContain("evt-regex-fail");
|
|
109
|
+
expect(urlResult === null || urlResult === void 0 ? void 0 : urlResult.passedEventIds).not.toContain("evt-regex-fail");
|
|
110
|
+
});
|
|
111
|
+
test("logs warning and passes type check when regex syntax is invalid", () => {
|
|
112
|
+
const spec = {
|
|
113
|
+
eventName: "filter",
|
|
114
|
+
properties: [
|
|
115
|
+
{
|
|
116
|
+
propertyName: "pattern",
|
|
117
|
+
propertyType: "string",
|
|
118
|
+
regex: "[invalid",
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
const eventProperties = [
|
|
123
|
+
{ propertyName: "pattern", propertyType: "string", propertyValue: "anything" },
|
|
124
|
+
];
|
|
125
|
+
const results = validator.validate(spec, eventProperties, "evt-bad-regex");
|
|
126
|
+
const patternResult = results.find((r) => r.propertyName === "pattern");
|
|
127
|
+
// Invalid regex is caught, property still passes type check
|
|
128
|
+
expect(patternResult === null || patternResult === void 0 ? void 0 : patternResult.failedEventIds).toEqual([]);
|
|
129
|
+
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining("[Avo Inspector] Warning"));
|
|
130
|
+
});
|
|
131
|
+
test("skips unsafe regex patterns with warning", () => {
|
|
132
|
+
const spec = {
|
|
133
|
+
eventName: "search",
|
|
134
|
+
properties: [
|
|
135
|
+
{
|
|
136
|
+
propertyName: "query",
|
|
137
|
+
propertyType: "string",
|
|
138
|
+
// Catastrophic backtracking pattern
|
|
139
|
+
regex: "(a+)+$",
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
const eventProperties = [
|
|
144
|
+
{ propertyName: "query", propertyType: "string", propertyValue: "aaa" },
|
|
145
|
+
];
|
|
146
|
+
const results = validator.validate(spec, eventProperties, "evt-5");
|
|
147
|
+
// Unsafe regex should be skipped, property still passes type check
|
|
148
|
+
const queryResult = results.find((r) => r.propertyName === "query");
|
|
149
|
+
// Bandwidth optimization: passedEventIds cleared because passed count >= failed count
|
|
150
|
+
expect(queryResult === null || queryResult === void 0 ? void 0 : queryResult.failedEventIds).toEqual([]);
|
|
151
|
+
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining("[Avo Inspector] Warning"));
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
describe("extra/unexpected properties", () => {
|
|
155
|
+
test("ignores properties present in event but not in the spec", () => {
|
|
156
|
+
const spec = {
|
|
157
|
+
eventName: "click",
|
|
158
|
+
properties: [
|
|
159
|
+
{ propertyName: "target", propertyType: "string" },
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
const eventProperties = [
|
|
163
|
+
{ propertyName: "target", propertyType: "string" },
|
|
164
|
+
{ propertyName: "extraProp", propertyType: "int" },
|
|
165
|
+
{ propertyName: "anotherExtra", propertyType: "boolean" },
|
|
166
|
+
];
|
|
167
|
+
const results = validator.validate(spec, eventProperties, "evt-extra");
|
|
168
|
+
// Only spec properties get results — extra properties are not reported
|
|
169
|
+
expect(results).toHaveLength(1);
|
|
170
|
+
expect(results[0].propertyName).toBe("target");
|
|
171
|
+
expect(results.find((r) => r.propertyName === "extraProp")).toBeUndefined();
|
|
172
|
+
expect(results.find((r) => r.propertyName === "anotherExtra")).toBeUndefined();
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
describe("bandwidth optimization", () => {
|
|
176
|
+
test("returns passedEventIds only when strictly smaller than failedEventIds", () => {
|
|
177
|
+
const spec = {
|
|
178
|
+
eventName: "click",
|
|
179
|
+
properties: [{ propertyName: "target", propertyType: "string" }],
|
|
180
|
+
};
|
|
181
|
+
// Single pass
|
|
182
|
+
const passResults = validator.validate(spec, [{ propertyName: "target", propertyType: "string" }], "evt-pass");
|
|
183
|
+
const passResult = passResults.find((r) => r.propertyName === "target");
|
|
184
|
+
// When there's only passed and no failed, passedEventIds count (1) > failedEventIds count (0)
|
|
185
|
+
// So passedEventIds is NOT strictly smaller - it should be returned
|
|
186
|
+
// But the optimization says: return passedEventIds ONLY when strictly smaller than failedEventIds
|
|
187
|
+
// So when passedEventIds.length >= failedEventIds.length, we clear passedEventIds
|
|
188
|
+
expect(passResult === null || passResult === void 0 ? void 0 : passResult.passedEventIds).toEqual([]);
|
|
189
|
+
// Single fail
|
|
190
|
+
const failResults = validator.validate(spec, [{ propertyName: "target", propertyType: "int" }], "evt-fail");
|
|
191
|
+
const failResult = failResults.find((r) => r.propertyName === "target");
|
|
192
|
+
// failedEventIds has 1, passedEventIds has 0 -> passedEventIds is strictly smaller, return it
|
|
193
|
+
expect(failResult === null || failResult === void 0 ? void 0 : failResult.failedEventIds).toContain("evt-fail");
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -9,7 +9,6 @@ describe("NetworkCallsHandler", () => {
|
|
|
9
9
|
const appName = "";
|
|
10
10
|
let networkHandler;
|
|
11
11
|
let baseBody;
|
|
12
|
-
const customCallback = jest.fn();
|
|
13
12
|
const now = new Date();
|
|
14
13
|
beforeAll(() => {
|
|
15
14
|
// @ts-ignore
|
|
@@ -27,23 +26,20 @@ describe("NetworkCallsHandler", () => {
|
|
|
27
26
|
libPlatform: "node",
|
|
28
27
|
messageId: constants_1.mockedReturns.GUID,
|
|
29
28
|
trackingId: "",
|
|
29
|
+
sessionId: "",
|
|
30
|
+
anonymousId: "",
|
|
30
31
|
createdAt: new Date().toISOString(),
|
|
31
|
-
sessionId: constants_1.mockedReturns.SESSION_ID,
|
|
32
32
|
samplingRate: 1.0,
|
|
33
33
|
};
|
|
34
34
|
});
|
|
35
35
|
afterEach(() => {
|
|
36
36
|
jest.clearAllMocks();
|
|
37
37
|
});
|
|
38
|
-
test("bodyForSessionStartedCall returns base body + session started body used for session started", () => {
|
|
39
|
-
const body = networkHandler.bodyForSessionStartedCall(constants_1.mockedReturns.SESSION_ID);
|
|
40
|
-
expect(body).toEqual(Object.assign(Object.assign({}, baseBody), { type: "sessionStarted" }));
|
|
41
|
-
});
|
|
42
38
|
test("bodyForEventSchemaCall returns base body + event schema used for event sending from non Codegen", () => {
|
|
43
39
|
const eventName = "event name";
|
|
44
40
|
const eventProperties = [{ propertyName: "prop0", propertyType: "string" }];
|
|
45
|
-
const body = networkHandler.bodyForEventSchemaCall(
|
|
46
|
-
expect(body).toEqual(Object.assign(Object.assign({}, baseBody), { type: "event", eventName,
|
|
41
|
+
const body = networkHandler.bodyForEventSchemaCall("", eventName, eventProperties, null, null);
|
|
42
|
+
expect(body).toEqual(Object.assign(Object.assign({}, baseBody), { anonymousId: "", type: "event", eventName,
|
|
47
43
|
eventProperties, avoFunction: false, eventId: null, eventHash: null }));
|
|
48
44
|
});
|
|
49
45
|
test("bodyForEventSchemaCall returns base body + event schema used for event sending from Codegen", () => {
|
|
@@ -51,9 +47,40 @@ describe("NetworkCallsHandler", () => {
|
|
|
51
47
|
const eventId = "event id";
|
|
52
48
|
const eventHash = "event hash";
|
|
53
49
|
const eventProperties = [{ propertyName: "prop0", propertyType: "string" }];
|
|
54
|
-
const body = networkHandler.bodyForEventSchemaCall(
|
|
55
|
-
expect(body).toEqual(Object.assign(Object.assign({}, baseBody), { type: "event", eventName,
|
|
50
|
+
const body = networkHandler.bodyForEventSchemaCall("", eventName, eventProperties, eventId, eventHash);
|
|
51
|
+
expect(body).toEqual(Object.assign(Object.assign({}, baseBody), { anonymousId: "", type: "event", eventName,
|
|
56
52
|
eventProperties, avoFunction: true, eventId,
|
|
57
53
|
eventHash }));
|
|
58
54
|
});
|
|
55
|
+
test("bodyForEventSchemaCall uses streamId as anonymousId", () => {
|
|
56
|
+
const eventName = "event name";
|
|
57
|
+
const streamId = "user-123";
|
|
58
|
+
const eventProperties = [{ propertyName: "prop0", propertyType: "string" }];
|
|
59
|
+
const body = networkHandler.bodyForEventSchemaCall(streamId, eventName, eventProperties, null, null);
|
|
60
|
+
expect(body.anonymousId).toBe("user-123");
|
|
61
|
+
});
|
|
62
|
+
test("bodyForEventSchemaCall with rawEventProperties passes them through (no encryption in dev without key)", () => {
|
|
63
|
+
const eventName = "event name";
|
|
64
|
+
const eventProperties = [{ propertyName: "prop0", propertyType: "string" }];
|
|
65
|
+
const rawEventProperties = { prop0: "hello" };
|
|
66
|
+
const body = networkHandler.bodyForEventSchemaCall("", eventName, eventProperties, null, null, rawEventProperties);
|
|
67
|
+
// Without a publicEncryptionKey, encryption is not triggered,
|
|
68
|
+
// so eventProperties should be the plain schema (not encrypted)
|
|
69
|
+
expect(body.eventProperties).toEqual(eventProperties);
|
|
70
|
+
expect(body.type).toBe("event");
|
|
71
|
+
expect(body.eventName).toBe(eventName);
|
|
72
|
+
});
|
|
73
|
+
test("base body includes publicEncryptionKey when handler is constructed with one", () => {
|
|
74
|
+
const handlerWithKey = new AvoNetworkCallsHandler_1.AvoNetworkCallsHandler(apiKey, env, "", version, inspectorVersion, "test-public-key-abc");
|
|
75
|
+
const eventName = "event name";
|
|
76
|
+
const eventProperties = [{ propertyName: "prop0", propertyType: "string" }];
|
|
77
|
+
const body = handlerWithKey.bodyForEventSchemaCall("", eventName, eventProperties, null, null);
|
|
78
|
+
expect(body.publicEncryptionKey).toBe("test-public-key-abc");
|
|
79
|
+
});
|
|
80
|
+
test("base body does not include publicEncryptionKey when handler is constructed without one", () => {
|
|
81
|
+
const eventName = "event name";
|
|
82
|
+
const eventProperties = [{ propertyName: "prop0", propertyType: "string" }];
|
|
83
|
+
const body = networkHandler.bodyForEventSchemaCall("", eventName, eventProperties, null, null);
|
|
84
|
+
expect(body.publicEncryptionKey).toBeUndefined();
|
|
85
|
+
});
|
|
59
86
|
});
|
|
@@ -25,7 +25,6 @@ describe("AvoNetworkCallsHandler", () => {
|
|
|
25
25
|
prop4: 0.0,
|
|
26
26
|
};
|
|
27
27
|
const happyPathInspectorCall = () => {
|
|
28
|
-
inspector.avoNetworkCallsHandler["samplingRate"] = 0.1;
|
|
29
28
|
return Promise.resolve();
|
|
30
29
|
};
|
|
31
30
|
beforeAll(() => {
|
|
@@ -43,12 +42,13 @@ describe("AvoNetworkCallsHandler", () => {
|
|
|
43
42
|
// @ts-ignore
|
|
44
43
|
inspector.avoDeduplicator._clearEvents();
|
|
45
44
|
});
|
|
46
|
-
test("
|
|
45
|
+
test("trackSchemaFromEvent sends only event call (no sessionStarted)", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
47
46
|
yield inspector.trackSchemaFromEvent(eventName, properties);
|
|
48
|
-
|
|
47
|
+
// Should be called once (event only, no sessionStarted)
|
|
48
|
+
expect(inspector.avoNetworkCallsHandler.callInspectorWithBatchBody).toHaveBeenCalledTimes(1);
|
|
49
49
|
expect(inspector.avoNetworkCallsHandler.callInspectorWithBatchBody).toHaveBeenCalledWith([
|
|
50
50
|
expect.objectContaining({
|
|
51
|
-
type: "
|
|
51
|
+
type: "event",
|
|
52
52
|
apiKey: apiKey,
|
|
53
53
|
appName: appName,
|
|
54
54
|
appVersion: version,
|
|
@@ -56,51 +56,44 @@ describe("AvoNetworkCallsHandler", () => {
|
|
|
56
56
|
env: env,
|
|
57
57
|
libPlatform: "node",
|
|
58
58
|
messageId: expect.anything(),
|
|
59
|
-
|
|
59
|
+
anonymousId: "",
|
|
60
60
|
createdAt: expect.any(String),
|
|
61
|
-
sessionId: expect.anything(),
|
|
62
61
|
samplingRate: 1,
|
|
63
62
|
}),
|
|
64
63
|
]);
|
|
64
|
+
}));
|
|
65
|
+
test("trackSchemaFromEvent with streamId sets anonymousId to streamId", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
66
|
+
yield inspector.trackSchemaFromEvent(eventName, properties, "user-stream-123");
|
|
67
|
+
expect(inspector.avoNetworkCallsHandler.callInspectorWithBatchBody).toHaveBeenCalledTimes(1);
|
|
65
68
|
expect(inspector.avoNetworkCallsHandler.callInspectorWithBatchBody).toHaveBeenCalledWith([
|
|
66
69
|
expect.objectContaining({
|
|
67
70
|
type: "event",
|
|
68
|
-
|
|
69
|
-
appName: appName,
|
|
70
|
-
appVersion: version,
|
|
71
|
-
libVersion: expect.anything(),
|
|
72
|
-
env: env,
|
|
73
|
-
libPlatform: "node",
|
|
74
|
-
messageId: expect.anything(),
|
|
75
|
-
trackingId: "",
|
|
76
|
-
createdAt: expect.anything(),
|
|
77
|
-
sessionId: expect.anything(),
|
|
78
|
-
samplingRate: 0.1,
|
|
71
|
+
anonymousId: "user-stream-123",
|
|
79
72
|
}),
|
|
80
73
|
]);
|
|
81
74
|
}));
|
|
82
|
-
test("
|
|
75
|
+
test("trackSchemaFromEvent without streamId sets anonymousId to empty string", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
76
|
+
yield inspector.trackSchemaFromEvent(eventName, properties);
|
|
77
|
+
expect(inspector.avoNetworkCallsHandler.callInspectorWithBatchBody).toHaveBeenCalledTimes(1);
|
|
78
|
+
expect(inspector.avoNetworkCallsHandler.callInspectorWithBatchBody).toHaveBeenCalledWith([
|
|
79
|
+
expect.objectContaining({
|
|
80
|
+
anonymousId: "",
|
|
81
|
+
}),
|
|
82
|
+
]);
|
|
83
|
+
}));
|
|
84
|
+
test("trackSchemaFromEvent with streamId containing ':' logs warning", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
85
|
+
const consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => { });
|
|
86
|
+
yield inspector.trackSchemaFromEvent(eventName, properties, "invalid:stream");
|
|
87
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith("[Avo Inspector] Warning: streamId contains ':' which is not supported");
|
|
88
|
+
consoleWarnSpy.mockRestore();
|
|
89
|
+
}));
|
|
90
|
+
test("_avoFunctionTrackSchemaFromEvent sends only event call (no sessionStarted)", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
83
91
|
const eventId = "testId";
|
|
84
92
|
const eventHash = "testHash";
|
|
85
93
|
// @ts-ignore
|
|
86
94
|
yield inspector._avoFunctionTrackSchemaFromEvent(eventName, properties, eventId, eventHash);
|
|
87
|
-
|
|
88
|
-
expect(inspector.avoNetworkCallsHandler.callInspectorWithBatchBody).
|
|
89
|
-
expect.objectContaining({
|
|
90
|
-
type: "sessionStarted",
|
|
91
|
-
apiKey: apiKey,
|
|
92
|
-
appName: appName,
|
|
93
|
-
appVersion: version,
|
|
94
|
-
libVersion: expect.anything(),
|
|
95
|
-
env: env,
|
|
96
|
-
libPlatform: "node",
|
|
97
|
-
messageId: expect.anything(),
|
|
98
|
-
trackingId: "",
|
|
99
|
-
createdAt: expect.anything(),
|
|
100
|
-
sessionId: expect.anything(),
|
|
101
|
-
samplingRate: 0.1,
|
|
102
|
-
})
|
|
103
|
-
]);
|
|
95
|
+
// Should be called once (event only, no sessionStarted)
|
|
96
|
+
expect(inspector.avoNetworkCallsHandler.callInspectorWithBatchBody).toHaveBeenCalledTimes(1);
|
|
104
97
|
expect(inspector.avoNetworkCallsHandler.callInspectorWithBatchBody).toHaveBeenCalledWith([
|
|
105
98
|
expect.objectContaining({
|
|
106
99
|
type: "event",
|
|
@@ -111,17 +104,57 @@ describe("AvoNetworkCallsHandler", () => {
|
|
|
111
104
|
env: env,
|
|
112
105
|
libPlatform: "node",
|
|
113
106
|
messageId: expect.anything(),
|
|
114
|
-
|
|
115
|
-
createdAt: expect.
|
|
116
|
-
sessionId: expect.anything(),
|
|
117
|
-
samplingRate: 0.1,
|
|
107
|
+
anonymousId: "",
|
|
108
|
+
createdAt: expect.any(String),
|
|
118
109
|
}),
|
|
119
110
|
]);
|
|
120
111
|
}));
|
|
121
112
|
test("an error is logged if event schema call fails and AvoInspector.shouldLog is true", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
122
|
-
|
|
113
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
123
114
|
inspectorCallSpy.mockRejectedValueOnce('Network error');
|
|
124
115
|
yield inspector.trackSchemaFromEvent(eventName, properties);
|
|
125
|
-
expect(
|
|
116
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('Avo Inspector: schema sending failed: Network error.');
|
|
117
|
+
consoleErrorSpy.mockRestore();
|
|
118
|
+
}));
|
|
119
|
+
test("_avoFunctionTrackSchemaFromEvent passes empty string as anonymousId", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
120
|
+
const eventId = "testId";
|
|
121
|
+
const eventHash = "testHash";
|
|
122
|
+
// @ts-ignore
|
|
123
|
+
yield inspector._avoFunctionTrackSchemaFromEvent(eventName, properties, eventId, eventHash);
|
|
124
|
+
expect(inspector.avoNetworkCallsHandler.callInspectorWithBatchBody).toHaveBeenCalledWith([
|
|
125
|
+
expect.objectContaining({
|
|
126
|
+
anonymousId: "",
|
|
127
|
+
}),
|
|
128
|
+
]);
|
|
129
|
+
}));
|
|
130
|
+
test("trackSchemaFromEvent with streamId containing ':' still sends the event", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
131
|
+
const consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => { });
|
|
132
|
+
yield inspector.trackSchemaFromEvent(eventName, properties, "bad:stream");
|
|
133
|
+
// Warning is logged
|
|
134
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith("[Avo Inspector] Warning: streamId contains ':' which is not supported");
|
|
135
|
+
// Event is still sent despite the warning
|
|
136
|
+
expect(inspector.avoNetworkCallsHandler.callInspectorWithBatchBody).toHaveBeenCalledTimes(1);
|
|
137
|
+
expect(inspector.avoNetworkCallsHandler.callInspectorWithBatchBody).toHaveBeenCalledWith([
|
|
138
|
+
expect.objectContaining({
|
|
139
|
+
type: "event",
|
|
140
|
+
anonymousId: "bad:stream",
|
|
141
|
+
}),
|
|
142
|
+
]);
|
|
143
|
+
consoleWarnSpy.mockRestore();
|
|
144
|
+
}));
|
|
145
|
+
test("same event with different streamIds is not deduplicated", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
146
|
+
yield inspector.trackSchemaFromEvent(eventName, properties, "stream-a");
|
|
147
|
+
yield inspector.trackSchemaFromEvent(eventName, properties, "stream-b");
|
|
148
|
+
expect(inspector.avoNetworkCallsHandler.callInspectorWithBatchBody).toHaveBeenCalledTimes(2);
|
|
149
|
+
}));
|
|
150
|
+
test("trackSchemaFromEvent rejection contains the expected error string", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
151
|
+
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => { });
|
|
152
|
+
// Force the catch block in trackSchemaFromEvent by making extractSchema throw
|
|
153
|
+
jest.spyOn(inspector, "extractSchema").mockImplementation(() => {
|
|
154
|
+
throw new Error("test explosion");
|
|
155
|
+
});
|
|
156
|
+
yield expect(inspector.trackSchemaFromEvent(eventName, properties)).rejects.toBe("Avo Inspector: something went wrong. Please report to support@avo.app.");
|
|
157
|
+
consoleErrorSpy.mockRestore();
|
|
158
|
+
inspector.extractSchema.mockRestore();
|
|
126
159
|
}));
|
|
127
160
|
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { EventSpecResponse } from "./AvoEventSpecFetchTypes";
|
|
2
|
+
export declare class AvoEventSpecCache {
|
|
3
|
+
private cache;
|
|
4
|
+
private globalEventCount;
|
|
5
|
+
static makeKey(apiKey: string, streamId: string, eventName: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Returns the cached EventSpecResponse, or undefined on cache miss.
|
|
8
|
+
* A cached response with eventSpec: null means "no spec exists for this event".
|
|
9
|
+
*/
|
|
10
|
+
get(key: string): EventSpecResponse | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Returns true if the key exists in the cache and has not expired.
|
|
13
|
+
*/
|
|
14
|
+
contains(key: string): boolean;
|
|
15
|
+
set(key: string, value: EventSpecResponse): void;
|
|
16
|
+
flush(): void;
|
|
17
|
+
private purgeExpired;
|
|
18
|
+
private evictLRU;
|
|
19
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AvoEventSpecCache = void 0;
|
|
4
|
+
const MAX_EVENT_COUNT = 50;
|
|
5
|
+
const TTL_MS = 60000;
|
|
6
|
+
class AvoEventSpecCache {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.cache = new Map();
|
|
9
|
+
this.globalEventCount = 0;
|
|
10
|
+
}
|
|
11
|
+
static makeKey(apiKey, streamId, eventName) {
|
|
12
|
+
return `${apiKey}\0${streamId}\0${eventName}`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Returns the cached EventSpecResponse, or undefined on cache miss.
|
|
16
|
+
* A cached response with eventSpec: null means "no spec exists for this event".
|
|
17
|
+
*/
|
|
18
|
+
get(key) {
|
|
19
|
+
const entry = this.cache.get(key);
|
|
20
|
+
if (entry === undefined) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
// TTL check
|
|
24
|
+
if (Date.now() - entry.timestamp > TTL_MS) {
|
|
25
|
+
this.cache.delete(key);
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
// Per-entry access count eviction
|
|
29
|
+
entry.eventCount++;
|
|
30
|
+
if (entry.eventCount >= MAX_EVENT_COUNT) {
|
|
31
|
+
this.cache.delete(key);
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
entry.lastAccessed = Date.now();
|
|
35
|
+
return entry.value;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Returns true if the key exists in the cache and has not expired.
|
|
39
|
+
*/
|
|
40
|
+
contains(key) {
|
|
41
|
+
const entry = this.cache.get(key);
|
|
42
|
+
if (entry === undefined) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (Date.now() - entry.timestamp > TTL_MS) {
|
|
46
|
+
this.cache.delete(key);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (entry.eventCount >= MAX_EVENT_COUNT) {
|
|
50
|
+
this.cache.delete(key);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
set(key, value) {
|
|
56
|
+
this.globalEventCount++;
|
|
57
|
+
// Global rotation: evict LRU entry every MAX_EVENT_COUNT operations
|
|
58
|
+
if (this.globalEventCount >= MAX_EVENT_COUNT) {
|
|
59
|
+
this.purgeExpired();
|
|
60
|
+
this.evictLRU();
|
|
61
|
+
this.globalEventCount = 0;
|
|
62
|
+
}
|
|
63
|
+
this.cache.set(key, {
|
|
64
|
+
value,
|
|
65
|
+
timestamp: Date.now(),
|
|
66
|
+
eventCount: 0,
|
|
67
|
+
lastAccessed: Date.now(),
|
|
68
|
+
});
|
|
69
|
+
// Size cap: evict oldest entries when exceeding max
|
|
70
|
+
while (this.cache.size > MAX_EVENT_COUNT) {
|
|
71
|
+
this.evictLRU();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
flush() {
|
|
75
|
+
this.cache.clear();
|
|
76
|
+
this.globalEventCount = 0;
|
|
77
|
+
}
|
|
78
|
+
purgeExpired() {
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
81
|
+
if (now - entry.timestamp > TTL_MS) {
|
|
82
|
+
this.cache.delete(key);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
evictLRU() {
|
|
87
|
+
let oldestKey = null;
|
|
88
|
+
let oldestTime = Infinity;
|
|
89
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
90
|
+
if (entry.lastAccessed < oldestTime) {
|
|
91
|
+
oldestTime = entry.lastAccessed;
|
|
92
|
+
oldestKey = key;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (oldestKey !== null) {
|
|
96
|
+
this.cache.delete(oldestKey);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
exports.AvoEventSpecCache = AvoEventSpecCache;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for Event Spec validation responses.
|
|
3
|
+
*/
|
|
4
|
+
/** A single property constraint from the spec. */
|
|
5
|
+
export interface PropertyConstraint {
|
|
6
|
+
propertyName: string;
|
|
7
|
+
/** Expected type, e.g. "string", "int", "float", "boolean", "object", "list(string)" */
|
|
8
|
+
propertyType: string;
|
|
9
|
+
/** Optional regex pattern that the value must match */
|
|
10
|
+
regex?: string;
|
|
11
|
+
}
|
|
12
|
+
/** The spec for a single event, as returned by the API. */
|
|
13
|
+
export interface EventSpec {
|
|
14
|
+
eventName: string;
|
|
15
|
+
properties: PropertyConstraint[];
|
|
16
|
+
}
|
|
17
|
+
/** Metadata identifying the spec source. */
|
|
18
|
+
export interface EventSpecMetadata {
|
|
19
|
+
schemaId: string;
|
|
20
|
+
branchId: string;
|
|
21
|
+
latestActionId: string;
|
|
22
|
+
sourceId: string;
|
|
23
|
+
}
|
|
24
|
+
/** Full response from the spec endpoint. */
|
|
25
|
+
export interface EventSpecResponse {
|
|
26
|
+
eventSpec: EventSpec | null;
|
|
27
|
+
metadata: EventSpecMetadata;
|
|
28
|
+
}
|
|
29
|
+
/** Per-property validation result. */
|
|
30
|
+
export interface PropertyValidationResult {
|
|
31
|
+
propertyName: string;
|
|
32
|
+
failedEventIds: string[];
|
|
33
|
+
passedEventIds: string[];
|
|
34
|
+
}
|
|
35
|
+
/** Payload sent to reportValidatedEvent. */
|
|
36
|
+
export interface ValidatedEventPayload {
|
|
37
|
+
streamId: string;
|
|
38
|
+
eventName: string;
|
|
39
|
+
eventSpecMetadata: EventSpecMetadata;
|
|
40
|
+
propertyResults: PropertyValidationResult[];
|
|
41
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { EventSpecResponse } from "./AvoEventSpecFetchTypes";
|
|
2
|
+
type FetchCallback = (result: EventSpecResponse | null) => void;
|
|
3
|
+
export declare class AvoEventSpecFetcher {
|
|
4
|
+
private apiKey;
|
|
5
|
+
private inFlight;
|
|
6
|
+
private agent;
|
|
7
|
+
private static specEndpoint;
|
|
8
|
+
constructor(apiKey: string);
|
|
9
|
+
fetch(eventName: string, streamId: string, callback: FetchCallback): void;
|
|
10
|
+
private resolveCallbacks;
|
|
11
|
+
/**
|
|
12
|
+
* Parse the wire format from /trackingPlan/eventSpec into our internal types.
|
|
13
|
+
*
|
|
14
|
+
* Wire format:
|
|
15
|
+
* { events: [{ b, id, vids, p: { "PropName": { t: "string", r: true, v: {...}, rx: {...} } } }],
|
|
16
|
+
* metadata: { schemaId, branchId, latestActionId, sourceId } }
|
|
17
|
+
*
|
|
18
|
+
* Internal format:
|
|
19
|
+
* { eventSpec: { eventName, properties: [{ propertyName, propertyType, regex? }] } | null,
|
|
20
|
+
* metadata: { schemaId, branchId, latestActionId, sourceId } }
|
|
21
|
+
*/
|
|
22
|
+
static parseWireResponse(wire: any, eventName: string): EventSpecResponse;
|
|
23
|
+
destroy(): void;
|
|
24
|
+
}
|
|
25
|
+
export {};
|