@oneuptime/common 12.0.17 → 12.0.19

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.
Files changed (220) hide show
  1. package/Models/DatabaseModels/EnterpriseLicense.ts +23 -0
  2. package/Models/DatabaseModels/Index.ts +4 -2
  3. package/Models/DatabaseModels/Monitor.ts +39 -9
  4. package/Models/DatabaseModels/UserMicrosoftTeams.ts +344 -0
  5. package/Models/DatabaseModels/UserNotificationRule.ts +144 -8
  6. package/Models/DatabaseModels/UserNotificationSetting.ts +34 -0
  7. package/Models/DatabaseModels/UserOnCallLogTimeline.ts +95 -0
  8. package/Models/DatabaseModels/UserSlack.ts +341 -0
  9. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +9 -0
  10. package/Server/API/UserMicrosoftTeamsAPI.ts +127 -0
  11. package/Server/API/UserSlackAPI.ts +125 -0
  12. package/Server/EnvironmentConfig.ts +12 -107
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.ts +33 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.ts +51 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +117 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +197 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  18. package/Server/Infrastructure/Queue.ts +7 -0
  19. package/Server/Services/BillingService.ts +8 -0
  20. package/Server/Services/EnterpriseLicenseService.ts +60 -0
  21. package/Server/Services/Index.ts +6 -2
  22. package/Server/Services/LlmLogService.ts +25 -9
  23. package/Server/Services/OnCallReadinessService.ts +86 -2
  24. package/Server/Services/ProjectService.ts +67 -0
  25. package/Server/Services/UserMicrosoftTeamsService.ts +208 -0
  26. package/Server/Services/UserNotificationMethodAdminService.ts +167 -5
  27. package/Server/Services/UserNotificationRuleAdminService.ts +40 -6
  28. package/Server/Services/UserNotificationRuleService.ts +589 -11
  29. package/Server/Services/UserNotificationSettingService.ts +138 -0
  30. package/Server/Services/UserService.ts +28 -0
  31. package/Server/Services/UserSlackService.ts +218 -0
  32. package/Server/Services/WorkspaceProjectAuthTokenService.ts +67 -1
  33. package/Server/Services/WorkspaceUserAuthTokenService.ts +73 -0
  34. package/Server/Services/WorkspaceUserNotificationService.ts +190 -0
  35. package/Server/Utils/Marketing/MarketingEventUtil.ts +145 -0
  36. package/Server/Utils/Marketing/MarketingEventWebhook.ts +114 -0
  37. package/Server/Utils/Monitor/MonitorLogUtil.ts +14 -1
  38. package/Server/Utils/Monitor/MonitorPayloadRedaction.ts +321 -0
  39. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +191 -4
  40. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +22 -14
  41. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +22 -11
  42. package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +37 -1
  43. package/Tests/App/Dashboard/OnCallRulesTable.test.tsx +18 -10
  44. package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +17 -0
  45. package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +27 -1
  46. package/Tests/Models/DatabaseModels/MonitorSecretKeyColumnAccessControl.test.ts +214 -0
  47. package/Tests/Server/API/OnCallReadinessAPI.test.ts +145 -26
  48. package/Tests/Server/API/UserNotificationMethodAdminAPI.test.ts +2 -2
  49. package/Tests/Server/Services/AIServiceDailyBudget.test.ts +11 -1
  50. package/Tests/Server/Services/AdminRuleEditGuards.test.ts +22 -8
  51. package/Tests/Server/Services/BillingService.test.ts +20 -0
  52. package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +18 -2
  53. package/Tests/Server/Services/EpisodeRuleSeverityRepair.test.ts +5 -1
  54. package/Tests/Server/Services/LlmLogServiceTokenAggregateParams.test.ts +640 -0
  55. package/Tests/Server/Services/NotificationChannelEventCoverage.test.ts +246 -22
  56. package/Tests/Server/Services/NotificationDeletionImpact.test.ts +136 -8
  57. package/Tests/Server/Services/OnCallNotificationFallback.test.ts +191 -8
  58. package/Tests/Server/Services/OnCallReadinessService.test.ts +343 -18
  59. package/Tests/Server/Services/SeverityRuleBackfill.test.ts +5 -1
  60. package/Tests/Server/Services/UserMicrosoftTeamsService.test.ts +343 -0
  61. package/Tests/Server/Services/UserNotificationMethodAdminService.test.ts +209 -8
  62. package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +22 -6
  63. package/Tests/Server/Services/UserNotificationRuleDefaultCreation.test.ts +12 -6
  64. package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +37 -3
  65. package/Tests/Server/Services/UserNotificationRuleWorkspaceDelivery.test.ts +669 -0
  66. package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +410 -0
  67. package/Tests/Server/Services/UserSlackService.test.ts +407 -0
  68. package/Tests/Server/Services/WorkspaceProjectAuthTokenDisconnectCascade.test.ts +224 -0
  69. package/Tests/Server/Services/WorkspaceUserAuthTokenNotificationMethodCascade.test.ts +189 -0
  70. package/Tests/Server/Services/WorkspaceUserNotificationService.test.ts +496 -0
  71. package/Tests/Server/Types/Database/Permissions/AdminNotificationRuleAccess.test.ts +30 -17
  72. package/Tests/Server/Types/Database/Permissions/CreateOwnershipScoping.test.ts +8 -3
  73. package/Tests/Server/Types/Database/Permissions/OwnerOnlyColumns.test.ts +72 -3
  74. package/Tests/Server/Types/Database/Permissions/UserNotificationRuleScoping.test.ts +22 -18
  75. package/Tests/Server/Types/Database/Permissions/WorkspaceMethodStampedColumnCreate.test.ts +178 -0
  76. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +259 -0
  77. package/Tests/Server/Utils/Marketing/MarketingEventWebhook.test.ts +257 -0
  78. package/Tests/Server/Utils/Monitor/MonitorLogUtilRedaction.test.ts +300 -0
  79. package/Tests/Server/Utils/Monitor/MonitorPayloadRedaction.test.ts +381 -0
  80. package/Tests/Server/Utils/Runbook/RunbookExecutePermission.test.ts +228 -0
  81. package/Tests/Server/Utils/SessionReplay/SessionReplayErasureTombstone.test.ts +189 -0
  82. package/Tests/Server/Utils/SessionReplay/SessionReplayUsage.test.ts +205 -0
  83. package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +275 -0
  84. package/Tests/Server/Utils/Telemetry/TelemetryContext.test.ts +279 -0
  85. package/Tests/Server/Utils/Workspace/MicrosoftTeamsDirectMessage.test.ts +454 -0
  86. package/Tests/Types/Kubernetes/KubernetesObjectParsers.test.ts +794 -0
  87. package/Tests/Types/Kubernetes/KubernetesRightSizing.test.ts +53 -0
  88. package/Tests/Types/Measurement/MeasurementAggregationType.test.ts +100 -0
  89. package/Tests/Types/Monitor/MonitorStepConfigHelpers.test.ts +114 -0
  90. package/Tests/Types/Workspace/WorkspaceType.test.ts +53 -0
  91. package/Tests/UI/Utils/NotificationMethodUtil.test.ts +137 -9
  92. package/Tests/UI/Utils/PermissionGate.test.ts +105 -0
  93. package/Tests/Utils/Dashboard/Components/DashboardComponentDefaults.test.ts +257 -0
  94. package/Types/Billing/PayAsYouGoPricing.ts +1 -1
  95. package/Types/Marketing/MarketingEvent.ts +97 -0
  96. package/UI/Utils/NotificationMethodUtil.ts +41 -1
  97. package/UI/Utils/PermissionGate.ts +56 -0
  98. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +24 -0
  99. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  100. package/build/dist/Models/DatabaseModels/Index.js +4 -2
  101. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  102. package/build/dist/Models/DatabaseModels/Monitor.js +39 -9
  103. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  104. package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js +363 -0
  105. package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js.map +1 -0
  106. package/build/dist/Models/DatabaseModels/UserNotificationRule.js +145 -8
  107. package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
  108. package/build/dist/Models/DatabaseModels/UserNotificationSetting.js +38 -0
  109. package/build/dist/Models/DatabaseModels/UserNotificationSetting.js.map +1 -1
  110. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +96 -0
  111. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
  112. package/build/dist/Models/DatabaseModels/UserSlack.js +361 -0
  113. package/build/dist/Models/DatabaseModels/UserSlack.js.map +1 -0
  114. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  115. package/build/dist/Server/API/UserMicrosoftTeamsAPI.js +82 -0
  116. package/build/dist/Server/API/UserMicrosoftTeamsAPI.js.map +1 -0
  117. package/build/dist/Server/API/UserSlackAPI.js +81 -0
  118. package/build/dist/Server/API/UserSlackAPI.js.map +1 -0
  119. package/build/dist/Server/EnvironmentConfig.js +10 -70
  120. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  121. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js +26 -0
  122. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js.map +1 -0
  123. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js +37 -0
  124. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js.map +1 -0
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +106 -0
  126. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +1 -0
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +78 -0
  128. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -0
  129. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  130. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  131. package/build/dist/Server/Infrastructure/Queue.js +7 -0
  132. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  133. package/build/dist/Server/Services/BillingService.js +6 -0
  134. package/build/dist/Server/Services/BillingService.js.map +1 -1
  135. package/build/dist/Server/Services/EnterpriseLicenseService.js +65 -0
  136. package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
  137. package/build/dist/Server/Services/Index.js +6 -2
  138. package/build/dist/Server/Services/Index.js.map +1 -1
  139. package/build/dist/Server/Services/LlmLogService.js +25 -9
  140. package/build/dist/Server/Services/LlmLogService.js.map +1 -1
  141. package/build/dist/Server/Services/OnCallReadinessService.js +74 -2
  142. package/build/dist/Server/Services/OnCallReadinessService.js.map +1 -1
  143. package/build/dist/Server/Services/ProjectService.js +56 -0
  144. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  145. package/build/dist/Server/Services/UserMicrosoftTeamsService.js +183 -0
  146. package/build/dist/Server/Services/UserMicrosoftTeamsService.js.map +1 -0
  147. package/build/dist/Server/Services/UserNotificationMethodAdminService.js +138 -2
  148. package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -1
  149. package/build/dist/Server/Services/UserNotificationRuleAdminService.js +32 -4
  150. package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -1
  151. package/build/dist/Server/Services/UserNotificationRuleService.js +499 -67
  152. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  153. package/build/dist/Server/Services/UserNotificationSettingService.js +118 -0
  154. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  155. package/build/dist/Server/Services/UserService.js +26 -1
  156. package/build/dist/Server/Services/UserService.js.map +1 -1
  157. package/build/dist/Server/Services/UserSlackService.js +195 -0
  158. package/build/dist/Server/Services/UserSlackService.js.map +1 -0
  159. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js +64 -1
  160. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js.map +1 -1
  161. package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js +70 -0
  162. package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js.map +1 -1
  163. package/build/dist/Server/Services/WorkspaceUserNotificationService.js +129 -0
  164. package/build/dist/Server/Services/WorkspaceUserNotificationService.js.map +1 -0
  165. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +94 -0
  166. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -0
  167. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +93 -0
  168. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -0
  169. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js +12 -1
  170. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js.map +1 -1
  171. package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js +233 -0
  172. package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js.map +1 -0
  173. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +141 -4
  174. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  175. package/build/dist/Types/Billing/PayAsYouGoPricing.js +1 -1
  176. package/build/dist/Types/Billing/PayAsYouGoPricing.js.map +1 -1
  177. package/build/dist/Types/Marketing/MarketingEvent.js +43 -0
  178. package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -0
  179. package/build/dist/UI/Utils/NotificationMethodUtil.js +33 -1
  180. package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -1
  181. package/build/dist/UI/Utils/PermissionGate.js +32 -0
  182. package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
  183. package/package.json +1 -1
  184. package/Models/DatabaseModels/MarketingConversion.ts +0 -410
  185. package/Server/Services/MarketingConversionService.ts +0 -10
  186. package/Server/Utils/Marketing/ConversionUploadProvider.ts +0 -215
  187. package/Server/Utils/Marketing/ConversionUploadProviders.ts +0 -22
  188. package/Server/Utils/Marketing/Providers/GoogleAds.ts +0 -333
  189. package/Server/Utils/Marketing/Providers/LinkedIn.ts +0 -185
  190. package/Server/Utils/Marketing/Providers/Meta.ts +0 -182
  191. package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +0 -223
  192. package/Server/Utils/Marketing/Providers/Reddit.ts +0 -208
  193. package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +0 -315
  194. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +0 -300
  195. package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +0 -592
  196. package/Tests/Server/Utils/Marketing/LinkedIn.test.ts +0 -282
  197. package/Tests/Server/Utils/Marketing/Meta.test.ts +0 -304
  198. package/Tests/Server/Utils/Marketing/MicrosoftAds.test.ts +0 -263
  199. package/Tests/Server/Utils/Marketing/Reddit.test.ts +0 -259
  200. package/Types/Marketing/MarketingConversion.ts +0 -53
  201. package/build/dist/Models/DatabaseModels/MarketingConversion.js +0 -445
  202. package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +0 -1
  203. package/build/dist/Server/Services/MarketingConversionService.js +0 -9
  204. package/build/dist/Server/Services/MarketingConversionService.js.map +0 -1
  205. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +0 -93
  206. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +0 -1
  207. package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js +0 -20
  208. package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js.map +0 -1
  209. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +0 -221
  210. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +0 -1
  211. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +0 -137
  212. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +0 -1
  213. package/build/dist/Server/Utils/Marketing/Providers/Meta.js +0 -133
  214. package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +0 -1
  215. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +0 -143
  216. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +0 -1
  217. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +0 -145
  218. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +0 -1
  219. package/build/dist/Types/Marketing/MarketingConversion.js +0 -52
  220. package/build/dist/Types/Marketing/MarketingConversion.js.map +0 -1
@@ -1,282 +0,0 @@
1
- import Attribution from "../../../../Server/Utils/Attribution";
2
- import MarketingConversion from "../../../../Models/DatabaseModels/MarketingConversion";
3
- import LinkedInProvider from "../../../../Server/Utils/Marketing/Providers/LinkedIn";
4
- import { ConversionSkip } from "../../../../Server/Utils/Marketing/ConversionUploadProvider";
5
- import { JSONObject } from "../../../../Types/JSON";
6
- import { MarketingConversionType } from "../../../../Types/Marketing/MarketingConversion";
7
- import ObjectID from "../../../../Types/ObjectID";
8
- import axios, { AxiosError } from "axios";
9
- import {
10
- afterEach,
11
- beforeEach,
12
- describe,
13
- expect,
14
- jest,
15
- test,
16
- } from "@jest/globals";
17
- import { SpyInstance } from "jest-mock";
18
-
19
- /*
20
- * ---------------------------------------------------------------------------
21
- * LinkedIn Conversions API.
22
- *
23
- * LinkedIn is the platform where enhanced matching matters most for this
24
- * product: it is where enterprise buyers are advertised to, and an enterprise
25
- * cycle is exactly the case where the li_fat_id from the original click is
26
- * long gone by the time the deal closes. LinkedIn accepts several userIds per
27
- * event and resolves whichever it can, so both identifiers are sent.
28
- *
29
- * Conversion rules are per-type and operator-configured, so an unset rule id
30
- * must leave the conversion PENDING rather than discard it.
31
- * ---------------------------------------------------------------------------
32
- */
33
-
34
- jest.mock("../../../../Server/EnvironmentConfig", () => {
35
- return {
36
- LinkedInApiVersion: "202606",
37
- LinkedInConversionsAccessToken: "linkedin-token",
38
- LinkedInSignUpConversionId: "1111",
39
- LinkedInPaidSubscriptionConversionId: "2222",
40
- LinkedInMeetingBookedConversionId: "3333",
41
- };
42
- });
43
-
44
- type MakeConversionFunction = (
45
- data?: Partial<MarketingConversion>,
46
- ) => MarketingConversion;
47
-
48
- const makeConversion: MakeConversionFunction = (
49
- data: Partial<MarketingConversion> = {},
50
- ): MarketingConversion => {
51
- return Object.assign(new MarketingConversion(), {
52
- id: new ObjectID("33333333-3333-4333-8333-333333333333"),
53
- conversionType: MarketingConversionType.SignUp,
54
- conversionAt: new Date("2026-07-22T10:11:12.345Z"),
55
- clickIds: { li_fat_id: "linkedin-click" },
56
- ...data,
57
- });
58
- };
59
-
60
- type UploadedBodyFunction = (postSpy: SpyInstance<any>) => JSONObject;
61
-
62
- const uploadedBody: UploadedBodyFunction = (
63
- postSpy: SpyInstance<any>,
64
- ): JSONObject => {
65
- return postSpy.mock.calls[0]?.[1] as JSONObject;
66
- };
67
-
68
- describe("LinkedInProvider", () => {
69
- let provider: LinkedInProvider;
70
- let postSpy: SpyInstance<any>;
71
-
72
- beforeEach(() => {
73
- provider = new LinkedInProvider();
74
- postSpy = jest.spyOn(axios, "post") as SpyInstance<any>;
75
- jest
76
- .spyOn(Date, "now")
77
- .mockReturnValue(new Date("2026-07-23T10:11:12.345Z").getTime());
78
- });
79
-
80
- afterEach(() => {
81
- jest.restoreAllMocks();
82
- });
83
-
84
- test("reports configured when the access token exists", () => {
85
- expect(provider.isConfigured()).toBe(true);
86
- });
87
-
88
- describe("identifiers", () => {
89
- test("accepts a conversion with only a click id", () => {
90
- expect(provider.getSkipReason(makeConversion())).toBeNull();
91
- });
92
-
93
- test("accepts a conversion with only an email", () => {
94
- expect(
95
- provider.getSkipReason(
96
- makeConversion({ clickIds: {}, email: "ada@example.com" }),
97
- ),
98
- ).toBeNull();
99
- });
100
-
101
- test("permanently skips a conversion with neither", () => {
102
- expect(provider.getSkipReason(makeConversion({ clickIds: {} }))).toEqual({
103
- reason: "No LinkedIn click id (li_fat_id) and no email to match on",
104
- isPermanent: true,
105
- });
106
- });
107
-
108
- test("sends both identifiers when both are known", async () => {
109
- postSpy.mockResolvedValueOnce({} as never);
110
-
111
- await provider.upload([makeConversion({ email: "Ada@Example.com" })]);
112
-
113
- expect(uploadedBody(postSpy)["user"]).toEqual({
114
- userIds: [
115
- {
116
- idType: "LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID",
117
- idValue: "linkedin-click",
118
- },
119
- {
120
- idType: "SHA256_EMAIL",
121
- idValue: Attribution.hashEmail("ada@example.com"),
122
- },
123
- ],
124
- });
125
- });
126
-
127
- /*
128
- * The click id used to be sent as an empty idValue when absent, which is a
129
- * userId LinkedIn can never resolve rather than an absent one.
130
- */
131
- test("sends only the email identifier when there is no click id", async () => {
132
- postSpy.mockResolvedValueOnce({} as never);
133
-
134
- await provider.upload([
135
- makeConversion({ clickIds: {}, email: "ada@example.com" }),
136
- ]);
137
-
138
- expect(uploadedBody(postSpy)["user"]).toEqual({
139
- userIds: [
140
- {
141
- idType: "SHA256_EMAIL",
142
- idValue: Attribution.hashEmail("ada@example.com"),
143
- },
144
- ],
145
- });
146
- });
147
-
148
- test("never sends the address in the clear", async () => {
149
- postSpy.mockResolvedValueOnce({} as never);
150
-
151
- await provider.upload([makeConversion({ email: "ada@example.com" })]);
152
-
153
- expect(JSON.stringify(uploadedBody(postSpy))).not.toContain(
154
- "ada@example.com",
155
- );
156
- });
157
- });
158
-
159
- describe("conversion rules", () => {
160
- test.each([
161
- [MarketingConversionType.SignUp, "1111"],
162
- [MarketingConversionType.PaidSubscription, "2222"],
163
- [MarketingConversionType.MeetingBooked, "3333"],
164
- ])(
165
- "reports %s against its own conversion rule",
166
- async (
167
- conversionType: MarketingConversionType,
168
- expectedRuleId: string,
169
- ) => {
170
- postSpy.mockResolvedValueOnce({} as never);
171
-
172
- await provider.upload([
173
- makeConversion({ conversionType: conversionType }),
174
- ]);
175
-
176
- expect(uploadedBody(postSpy)["conversion"]).toBe(
177
- `urn:lla:llaPartnerConversion:${expectedRuleId}`,
178
- );
179
- },
180
- );
181
-
182
- test.each([MarketingConversionType.MeetingBooked])(
183
- "attaches no conversionValue to %s even when the row holds one",
184
- async (conversionType: MarketingConversionType) => {
185
- postSpy.mockResolvedValueOnce({} as never);
186
-
187
- await provider.upload([
188
- makeConversion({
189
- conversionType: conversionType,
190
- conversionValueInUSDCents: 500000,
191
- }),
192
- ]);
193
-
194
- expect(uploadedBody(postSpy)["conversionValue"]).toBeUndefined();
195
- },
196
- );
197
-
198
- test("attaches conversionValue to a paid subscription", async () => {
199
- postSpy.mockResolvedValueOnce({} as never);
200
-
201
- await provider.upload([
202
- makeConversion({
203
- conversionType: MarketingConversionType.PaidSubscription,
204
- conversionValueInUSDCents: 12999,
205
- }),
206
- ]);
207
-
208
- expect(uploadedBody(postSpy)["conversionValue"]).toEqual({
209
- currencyCode: "USD",
210
- amount: "129.99",
211
- });
212
- });
213
- });
214
-
215
- test("keys the event on the conversion row id so retries dedupe", async () => {
216
- postSpy.mockResolvedValueOnce({} as never);
217
-
218
- await provider.upload([makeConversion()]);
219
-
220
- expect(uploadedBody(postSpy)["eventId"]).toBe(
221
- "33333333-3333-4333-8333-333333333333",
222
- );
223
- });
224
-
225
- test("permanently skips a conversion outside LinkedIn's 90-day window", () => {
226
- const skip: ConversionSkip | null = provider.getSkipReason(
227
- makeConversion({ conversionAt: new Date("2026-01-01T00:00:00.000Z") }),
228
- );
229
-
230
- expect(skip).toEqual({
231
- reason: "Conversion older than LinkedIn's 90-day window",
232
- isPermanent: true,
233
- });
234
- });
235
-
236
- describe("failure handling", () => {
237
- type BuildAxiosErrorFunction = (status: number) => AxiosError;
238
-
239
- const buildAxiosError: BuildAxiosErrorFunction = (
240
- status: number,
241
- ): AxiosError => {
242
- const error: AxiosError = new AxiosError("rejected");
243
- Object.assign(error, { response: { status: status, data: {} } });
244
- return error;
245
- };
246
-
247
- test.each([400, 422])(
248
- "records a %s as a permanent per-event failure",
249
- async (status: number) => {
250
- postSpy.mockRejectedValueOnce(buildAxiosError(status) as never);
251
-
252
- const result: { permanentFailures: Map<number, string> } =
253
- await provider.upload([makeConversion()]);
254
-
255
- expect(result.permanentFailures.get(0)).toContain(`HTTP ${status}`);
256
- },
257
- );
258
-
259
- /*
260
- * Anything that is not a definitive validation rejection is transport
261
- * level. It must throw so the whole batch stays pending and is retried —
262
- * recording it as a permanent failure would discard a conversion because
263
- * the token had expired or LinkedIn had a bad minute.
264
- */
265
- test.each([401, 429, 500, 503])(
266
- "throws on a %s so the batch is retried",
267
- async (status: number) => {
268
- postSpy.mockRejectedValueOnce(buildAxiosError(status) as never);
269
-
270
- await expect(provider.upload([makeConversion()])).rejects.toBeDefined();
271
- },
272
- );
273
-
274
- test("throws on a network error with no response at all", async () => {
275
- postSpy.mockRejectedValueOnce(new Error("socket hang up") as never);
276
-
277
- await expect(provider.upload([makeConversion()])).rejects.toThrow(
278
- "socket hang up",
279
- );
280
- });
281
- });
282
- });
@@ -1,304 +0,0 @@
1
- import Attribution from "../../../../Server/Utils/Attribution";
2
- import MarketingConversion from "../../../../Models/DatabaseModels/MarketingConversion";
3
- import MetaProvider from "../../../../Server/Utils/Marketing/Providers/Meta";
4
- import { ConversionSkip } from "../../../../Server/Utils/Marketing/ConversionUploadProvider";
5
- import { JSONObject } from "../../../../Types/JSON";
6
- import { MarketingConversionType } from "../../../../Types/Marketing/MarketingConversion";
7
- import ObjectID from "../../../../Types/ObjectID";
8
- import axios, { AxiosResponse } from "axios";
9
- import {
10
- afterEach,
11
- beforeEach,
12
- describe,
13
- expect,
14
- jest,
15
- test,
16
- } from "@jest/globals";
17
- import { SpyInstance } from "jest-mock";
18
-
19
- /*
20
- * ---------------------------------------------------------------------------
21
- * Meta Conversions API.
22
- *
23
- * Two things this file is really about:
24
- *
25
- * - WHAT A CONVERSION IS CALLED. Meta's event names carry meaning to the
26
- * optimiser. `Purchase` demands a value and pulls the event into the
27
- * revenue optimisation pool; `Schedule` and `Lead` do not. The provider
28
- * used to send `isSignUp ? CompleteRegistration : Purchase`, so anything
29
- * that was not a signup was revenue by default.
30
- *
31
- * - HOW A PERSON IS IDENTIFIED. Meta matches on a click id, a hashed email,
32
- * or both. Requiring fbclid — which the provider used to — discarded every
33
- * conversion whose click id had not survived to the moment of conversion,
34
- * which is nearly all of a sales-led funnel.
35
- * ---------------------------------------------------------------------------
36
- */
37
-
38
- jest.mock("../../../../Server/EnvironmentConfig", () => {
39
- return {
40
- MetaConversionsPixelId: "pixel-id",
41
- MetaConversionsAccessToken: "meta-access-token",
42
- MetaGraphApiVersion: "v22.0",
43
- };
44
- });
45
-
46
- type MakeConversionFunction = (
47
- data?: Partial<MarketingConversion>,
48
- ) => MarketingConversion;
49
-
50
- const makeConversion: MakeConversionFunction = (
51
- data: Partial<MarketingConversion> = {},
52
- ): MarketingConversion => {
53
- return Object.assign(new MarketingConversion(), {
54
- id: new ObjectID("11111111-1111-4111-8111-111111111111"),
55
- conversionType: MarketingConversionType.SignUp,
56
- conversionAt: new Date("2026-07-22T10:11:12.345Z"),
57
- clickIds: { fbclid: "meta-click" },
58
- ...data,
59
- });
60
- };
61
-
62
- type ResponseFunction = (data: JSONObject) => AxiosResponse<JSONObject>;
63
-
64
- const response: ResponseFunction = (
65
- data: JSONObject,
66
- ): AxiosResponse<JSONObject> => {
67
- return { data } as AxiosResponse<JSONObject>;
68
- };
69
-
70
- type UploadedEventFunction = (postSpy: SpyInstance<any>) => JSONObject;
71
-
72
- const uploadedEvent: UploadedEventFunction = (
73
- postSpy: SpyInstance<any>,
74
- ): JSONObject => {
75
- const body: JSONObject = postSpy.mock.calls[0]?.[1] as JSONObject;
76
- return (body["data"] as Array<JSONObject>)[0]!;
77
- };
78
-
79
- describe("MetaProvider", () => {
80
- let provider: MetaProvider;
81
- let postSpy: SpyInstance<any>;
82
-
83
- beforeEach(() => {
84
- provider = new MetaProvider();
85
- postSpy = jest.spyOn(axios, "post") as SpyInstance<any>;
86
- // Meta only accepts events up to 7 days old; freeze inside that window.
87
- jest
88
- .spyOn(Date, "now")
89
- .mockReturnValue(new Date("2026-07-23T10:11:12.345Z").getTime());
90
- });
91
-
92
- afterEach(() => {
93
- jest.restoreAllMocks();
94
- });
95
-
96
- test("reports configured only when the pixel and token exist", () => {
97
- expect(provider.isConfigured()).toBe(true);
98
- });
99
-
100
- describe("identifiers", () => {
101
- test("accepts a conversion with only a click id", () => {
102
- expect(
103
- provider.getSkipReason(makeConversion({ clickIds: { fbclid: "x" } })),
104
- ).toBeNull();
105
- });
106
-
107
- test("accepts a conversion with only an email", () => {
108
- expect(
109
- provider.getSkipReason(
110
- makeConversion({ clickIds: {}, email: "ada@example.com" }),
111
- ),
112
- ).toBeNull();
113
- });
114
-
115
- test("permanently skips a conversion with neither", () => {
116
- expect(provider.getSkipReason(makeConversion({ clickIds: {} }))).toEqual({
117
- reason: "No Meta click id (fbclid) and no email to match on",
118
- isPermanent: true,
119
- });
120
- });
121
-
122
- test("permanently skips a conversion outside Meta's 7-day window", () => {
123
- const skip: ConversionSkip | null = provider.getSkipReason(
124
- makeConversion({
125
- conversionAt: new Date("2026-07-01T00:00:00.000Z"),
126
- }),
127
- );
128
-
129
- expect(skip).toEqual({
130
- reason: "Conversion older than Meta's 7-day upload window",
131
- isPermanent: true,
132
- });
133
- });
134
-
135
- test("sends fbc and a hashed email when both are known", async () => {
136
- postSpy.mockResolvedValueOnce(response({ events_received: 1 }) as never);
137
-
138
- await provider.upload([makeConversion({ email: "Ada@Example.com" })]);
139
-
140
- /*
141
- * fbc is fb.1.{creationTimeMs}.{fbclid} per Meta's spec, and the
142
- * creation time it uses is the conversion time — the original click
143
- * timestamp is not stored.
144
- */
145
- expect(uploadedEvent(postSpy)["user_data"]).toEqual({
146
- fbc: `fb.1.${new Date("2026-07-22T10:11:12.345Z").getTime()}.meta-click`,
147
- em: [Attribution.hashEmail("ada@example.com")],
148
- });
149
- });
150
-
151
- /*
152
- * fbc is built by splicing the click id into a template, so an absent
153
- * click id must omit the field entirely rather than send a well-formed
154
- * string ending in nothing.
155
- */
156
- test("omits fbc entirely when there is no click id", async () => {
157
- postSpy.mockResolvedValueOnce(response({ events_received: 1 }) as never);
158
-
159
- await provider.upload([
160
- makeConversion({ clickIds: {}, email: "ada@example.com" }),
161
- ]);
162
-
163
- const userData: JSONObject = uploadedEvent(postSpy)[
164
- "user_data"
165
- ] as JSONObject;
166
-
167
- expect(userData["fbc"]).toBeUndefined();
168
- expect(userData["em"]).toEqual([
169
- Attribution.hashEmail("ada@example.com"),
170
- ]);
171
- });
172
-
173
- test("never sends the address in the clear", async () => {
174
- postSpy.mockResolvedValueOnce(response({ events_received: 1 }) as never);
175
-
176
- await provider.upload([makeConversion({ email: "ada@example.com" })]);
177
-
178
- expect(JSON.stringify(postSpy.mock.calls[0]?.[1])).not.toContain(
179
- "ada@example.com",
180
- );
181
- });
182
- });
183
-
184
- describe("event naming", () => {
185
- test.each([
186
- [MarketingConversionType.SignUp, "CompleteRegistration"],
187
- [MarketingConversionType.MeetingBooked, "Schedule"],
188
- [MarketingConversionType.PaidSubscription, "Purchase"],
189
- ])(
190
- "sends %s as %s",
191
- async (
192
- conversionType: MarketingConversionType,
193
- expectedEventName: string,
194
- ) => {
195
- postSpy.mockResolvedValueOnce(
196
- response({ events_received: 1 }) as never,
197
- );
198
-
199
- await provider.upload([
200
- makeConversion({ conversionType: conversionType }),
201
- ]);
202
-
203
- expect(uploadedEvent(postSpy)["event_name"]).toBe(expectedEventName);
204
- },
205
- );
206
-
207
- /*
208
- * The regression this exists for: a booked meeting reported as a Purchase
209
- * carrying the row's value would teach Meta's optimiser that demos are
210
- * revenue.
211
- */
212
- test.each([MarketingConversionType.MeetingBooked])(
213
- "attaches no custom_data to %s even when the row holds a value",
214
- async (conversionType: MarketingConversionType) => {
215
- postSpy.mockResolvedValueOnce(
216
- response({ events_received: 1 }) as never,
217
- );
218
-
219
- await provider.upload([
220
- makeConversion({
221
- conversionType: conversionType,
222
- conversionValueInUSDCents: 500000,
223
- }),
224
- ]);
225
-
226
- const event: JSONObject = uploadedEvent(postSpy);
227
-
228
- expect(event["event_name"]).not.toBe("Purchase");
229
- expect(event["custom_data"]).toBeUndefined();
230
- },
231
- );
232
-
233
- /*
234
- * Purchase is the one event Meta rejects without a value, and one rejected
235
- * event fails the whole batch — so a custom-pricing subscription with no
236
- * known revenue must still send a value, even a zero one.
237
- */
238
- test("sends a zero value for a purchase whose revenue is unknown", async () => {
239
- postSpy.mockResolvedValueOnce(response({ events_received: 1 }) as never);
240
-
241
- await provider.upload([
242
- makeConversion({
243
- conversionType: MarketingConversionType.PaidSubscription,
244
- }),
245
- ]);
246
-
247
- expect(uploadedEvent(postSpy)["custom_data"]).toEqual({
248
- value: 0,
249
- currency: "USD",
250
- });
251
- });
252
-
253
- test("sends the real value for a purchase that has one", async () => {
254
- postSpy.mockResolvedValueOnce(response({ events_received: 1 }) as never);
255
-
256
- await provider.upload([
257
- makeConversion({
258
- conversionType: MarketingConversionType.PaidSubscription,
259
- conversionValueInUSDCents: 12999,
260
- }),
261
- ]);
262
-
263
- expect(uploadedEvent(postSpy)["custom_data"]).toEqual({
264
- value: 129.99,
265
- currency: "USD",
266
- });
267
- });
268
- });
269
-
270
- describe("delivery", () => {
271
- test("keys the event on the conversion row id so retries dedupe", async () => {
272
- postSpy.mockResolvedValueOnce(response({ events_received: 1 }) as never);
273
-
274
- await provider.upload([makeConversion()]);
275
-
276
- expect(uploadedEvent(postSpy)["event_id"]).toBe(
277
- "11111111-1111-4111-8111-111111111111",
278
- );
279
- });
280
-
281
- test("posts to the configured pixel", async () => {
282
- postSpy.mockResolvedValueOnce(response({ events_received: 1 }) as never);
283
-
284
- await provider.upload([makeConversion()]);
285
-
286
- expect(postSpy.mock.calls[0]?.[0]).toBe(
287
- "https://graph.facebook.com/v22.0/pixel-id/events",
288
- );
289
- });
290
-
291
- /*
292
- * A partial acceptance is thrown, not recorded as a per-row failure: the
293
- * response does not say WHICH events were dropped, so the only safe move is
294
- * to retry the batch, which event_id makes idempotent.
295
- */
296
- test("throws when Meta accepts fewer events than were sent", async () => {
297
- postSpy.mockResolvedValueOnce(response({ events_received: 1 }) as never);
298
-
299
- await expect(
300
- provider.upload([makeConversion(), makeConversion()]),
301
- ).rejects.toThrow("Meta accepted 1 of 2 events");
302
- });
303
- });
304
- });