@oneuptime/common 12.0.18 → 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 (206) 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/EnterpriseLicenseService.ts +60 -0
  20. package/Server/Services/Index.ts +6 -2
  21. package/Server/Services/LlmLogService.ts +25 -9
  22. package/Server/Services/OnCallReadinessService.ts +86 -2
  23. package/Server/Services/ProjectService.ts +67 -0
  24. package/Server/Services/UserMicrosoftTeamsService.ts +208 -0
  25. package/Server/Services/UserNotificationMethodAdminService.ts +167 -5
  26. package/Server/Services/UserNotificationRuleAdminService.ts +40 -6
  27. package/Server/Services/UserNotificationRuleService.ts +589 -11
  28. package/Server/Services/UserNotificationSettingService.ts +138 -0
  29. package/Server/Services/UserService.ts +28 -0
  30. package/Server/Services/UserSlackService.ts +218 -0
  31. package/Server/Services/WorkspaceProjectAuthTokenService.ts +67 -1
  32. package/Server/Services/WorkspaceUserAuthTokenService.ts +73 -0
  33. package/Server/Services/WorkspaceUserNotificationService.ts +190 -0
  34. package/Server/Utils/Marketing/MarketingEventUtil.ts +145 -0
  35. package/Server/Utils/Marketing/MarketingEventWebhook.ts +114 -0
  36. package/Server/Utils/Monitor/MonitorLogUtil.ts +14 -1
  37. package/Server/Utils/Monitor/MonitorPayloadRedaction.ts +321 -0
  38. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +191 -4
  39. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +22 -14
  40. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +22 -11
  41. package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +37 -1
  42. package/Tests/App/Dashboard/OnCallRulesTable.test.tsx +18 -10
  43. package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +27 -1
  44. package/Tests/Models/DatabaseModels/MonitorSecretKeyColumnAccessControl.test.ts +214 -0
  45. package/Tests/Server/API/OnCallReadinessAPI.test.ts +145 -26
  46. package/Tests/Server/API/UserNotificationMethodAdminAPI.test.ts +2 -2
  47. package/Tests/Server/Services/AIServiceDailyBudget.test.ts +11 -1
  48. package/Tests/Server/Services/AdminRuleEditGuards.test.ts +22 -8
  49. package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +18 -2
  50. package/Tests/Server/Services/EpisodeRuleSeverityRepair.test.ts +5 -1
  51. package/Tests/Server/Services/LlmLogServiceTokenAggregateParams.test.ts +640 -0
  52. package/Tests/Server/Services/NotificationChannelEventCoverage.test.ts +246 -22
  53. package/Tests/Server/Services/NotificationDeletionImpact.test.ts +136 -8
  54. package/Tests/Server/Services/OnCallNotificationFallback.test.ts +191 -8
  55. package/Tests/Server/Services/OnCallReadinessService.test.ts +343 -18
  56. package/Tests/Server/Services/SeverityRuleBackfill.test.ts +5 -1
  57. package/Tests/Server/Services/UserMicrosoftTeamsService.test.ts +343 -0
  58. package/Tests/Server/Services/UserNotificationMethodAdminService.test.ts +209 -8
  59. package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +22 -6
  60. package/Tests/Server/Services/UserNotificationRuleDefaultCreation.test.ts +12 -6
  61. package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +37 -3
  62. package/Tests/Server/Services/UserNotificationRuleWorkspaceDelivery.test.ts +669 -0
  63. package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +410 -0
  64. package/Tests/Server/Services/UserSlackService.test.ts +407 -0
  65. package/Tests/Server/Services/WorkspaceProjectAuthTokenDisconnectCascade.test.ts +224 -0
  66. package/Tests/Server/Services/WorkspaceUserAuthTokenNotificationMethodCascade.test.ts +189 -0
  67. package/Tests/Server/Services/WorkspaceUserNotificationService.test.ts +496 -0
  68. package/Tests/Server/Types/Database/Permissions/AdminNotificationRuleAccess.test.ts +30 -17
  69. package/Tests/Server/Types/Database/Permissions/CreateOwnershipScoping.test.ts +8 -3
  70. package/Tests/Server/Types/Database/Permissions/OwnerOnlyColumns.test.ts +72 -3
  71. package/Tests/Server/Types/Database/Permissions/UserNotificationRuleScoping.test.ts +22 -18
  72. package/Tests/Server/Types/Database/Permissions/WorkspaceMethodStampedColumnCreate.test.ts +178 -0
  73. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +259 -0
  74. package/Tests/Server/Utils/Marketing/MarketingEventWebhook.test.ts +257 -0
  75. package/Tests/Server/Utils/Monitor/MonitorLogUtilRedaction.test.ts +300 -0
  76. package/Tests/Server/Utils/Monitor/MonitorPayloadRedaction.test.ts +381 -0
  77. package/Tests/Server/Utils/Runbook/RunbookExecutePermission.test.ts +228 -0
  78. package/Tests/Server/Utils/SessionReplay/SessionReplayErasureTombstone.test.ts +189 -0
  79. package/Tests/Server/Utils/SessionReplay/SessionReplayUsage.test.ts +205 -0
  80. package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +275 -0
  81. package/Tests/Server/Utils/Telemetry/TelemetryContext.test.ts +279 -0
  82. package/Tests/Server/Utils/Workspace/MicrosoftTeamsDirectMessage.test.ts +454 -0
  83. package/Tests/UI/Utils/NotificationMethodUtil.test.ts +137 -9
  84. package/Tests/UI/Utils/PermissionGate.test.ts +105 -0
  85. package/Types/Marketing/MarketingEvent.ts +97 -0
  86. package/UI/Utils/NotificationMethodUtil.ts +41 -1
  87. package/UI/Utils/PermissionGate.ts +56 -0
  88. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +24 -0
  89. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  90. package/build/dist/Models/DatabaseModels/Index.js +4 -2
  91. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  92. package/build/dist/Models/DatabaseModels/Monitor.js +39 -9
  93. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  94. package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js +363 -0
  95. package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js.map +1 -0
  96. package/build/dist/Models/DatabaseModels/UserNotificationRule.js +145 -8
  97. package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
  98. package/build/dist/Models/DatabaseModels/UserNotificationSetting.js +38 -0
  99. package/build/dist/Models/DatabaseModels/UserNotificationSetting.js.map +1 -1
  100. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +96 -0
  101. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
  102. package/build/dist/Models/DatabaseModels/UserSlack.js +361 -0
  103. package/build/dist/Models/DatabaseModels/UserSlack.js.map +1 -0
  104. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  105. package/build/dist/Server/API/UserMicrosoftTeamsAPI.js +82 -0
  106. package/build/dist/Server/API/UserMicrosoftTeamsAPI.js.map +1 -0
  107. package/build/dist/Server/API/UserSlackAPI.js +81 -0
  108. package/build/dist/Server/API/UserSlackAPI.js.map +1 -0
  109. package/build/dist/Server/EnvironmentConfig.js +10 -70
  110. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js +26 -0
  112. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js.map +1 -0
  113. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js +37 -0
  114. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js.map +1 -0
  115. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +106 -0
  116. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +1 -0
  117. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +78 -0
  118. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -0
  119. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  120. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  121. package/build/dist/Server/Infrastructure/Queue.js +7 -0
  122. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  123. package/build/dist/Server/Services/EnterpriseLicenseService.js +65 -0
  124. package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
  125. package/build/dist/Server/Services/Index.js +6 -2
  126. package/build/dist/Server/Services/Index.js.map +1 -1
  127. package/build/dist/Server/Services/LlmLogService.js +25 -9
  128. package/build/dist/Server/Services/LlmLogService.js.map +1 -1
  129. package/build/dist/Server/Services/OnCallReadinessService.js +74 -2
  130. package/build/dist/Server/Services/OnCallReadinessService.js.map +1 -1
  131. package/build/dist/Server/Services/ProjectService.js +56 -0
  132. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  133. package/build/dist/Server/Services/UserMicrosoftTeamsService.js +183 -0
  134. package/build/dist/Server/Services/UserMicrosoftTeamsService.js.map +1 -0
  135. package/build/dist/Server/Services/UserNotificationMethodAdminService.js +138 -2
  136. package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -1
  137. package/build/dist/Server/Services/UserNotificationRuleAdminService.js +32 -4
  138. package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -1
  139. package/build/dist/Server/Services/UserNotificationRuleService.js +499 -67
  140. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  141. package/build/dist/Server/Services/UserNotificationSettingService.js +118 -0
  142. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  143. package/build/dist/Server/Services/UserService.js +26 -1
  144. package/build/dist/Server/Services/UserService.js.map +1 -1
  145. package/build/dist/Server/Services/UserSlackService.js +195 -0
  146. package/build/dist/Server/Services/UserSlackService.js.map +1 -0
  147. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js +64 -1
  148. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js.map +1 -1
  149. package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js +70 -0
  150. package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js.map +1 -1
  151. package/build/dist/Server/Services/WorkspaceUserNotificationService.js +129 -0
  152. package/build/dist/Server/Services/WorkspaceUserNotificationService.js.map +1 -0
  153. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +94 -0
  154. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -0
  155. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +93 -0
  156. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -0
  157. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js +12 -1
  158. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js.map +1 -1
  159. package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js +233 -0
  160. package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js.map +1 -0
  161. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +141 -4
  162. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  163. package/build/dist/Types/Marketing/MarketingEvent.js +43 -0
  164. package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -0
  165. package/build/dist/UI/Utils/NotificationMethodUtil.js +33 -1
  166. package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -1
  167. package/build/dist/UI/Utils/PermissionGate.js +32 -0
  168. package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
  169. package/package.json +1 -1
  170. package/Models/DatabaseModels/MarketingConversion.ts +0 -410
  171. package/Server/Services/MarketingConversionService.ts +0 -10
  172. package/Server/Utils/Marketing/ConversionUploadProvider.ts +0 -215
  173. package/Server/Utils/Marketing/ConversionUploadProviders.ts +0 -22
  174. package/Server/Utils/Marketing/Providers/GoogleAds.ts +0 -333
  175. package/Server/Utils/Marketing/Providers/LinkedIn.ts +0 -185
  176. package/Server/Utils/Marketing/Providers/Meta.ts +0 -182
  177. package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +0 -223
  178. package/Server/Utils/Marketing/Providers/Reddit.ts +0 -208
  179. package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +0 -315
  180. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +0 -300
  181. package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +0 -592
  182. package/Tests/Server/Utils/Marketing/LinkedIn.test.ts +0 -282
  183. package/Tests/Server/Utils/Marketing/Meta.test.ts +0 -304
  184. package/Tests/Server/Utils/Marketing/MicrosoftAds.test.ts +0 -263
  185. package/Tests/Server/Utils/Marketing/Reddit.test.ts +0 -259
  186. package/Types/Marketing/MarketingConversion.ts +0 -53
  187. package/build/dist/Models/DatabaseModels/MarketingConversion.js +0 -445
  188. package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +0 -1
  189. package/build/dist/Server/Services/MarketingConversionService.js +0 -9
  190. package/build/dist/Server/Services/MarketingConversionService.js.map +0 -1
  191. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +0 -93
  192. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +0 -1
  193. package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js +0 -20
  194. package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js.map +0 -1
  195. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +0 -221
  196. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +0 -1
  197. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +0 -137
  198. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +0 -1
  199. package/build/dist/Server/Utils/Marketing/Providers/Meta.js +0 -133
  200. package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +0 -1
  201. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +0 -143
  202. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +0 -1
  203. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +0 -145
  204. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +0 -1
  205. package/build/dist/Types/Marketing/MarketingConversion.js +0 -52
  206. package/build/dist/Types/Marketing/MarketingConversion.js.map +0 -1
@@ -0,0 +1,190 @@
1
+ import BaseService from "./BaseService";
2
+ import UserOnCallLogTimelineService from "./UserOnCallLogTimelineService";
3
+ import WorkspaceNotificationLogService from "./WorkspaceNotificationLogService";
4
+ import WorkspaceProjectAuthTokenService from "./WorkspaceProjectAuthTokenService";
5
+ import SlackUtil from "../Utils/Workspace/Slack/Slack";
6
+ import MicrosoftTeamsUtil from "../Utils/Workspace/MicrosoftTeams/MicrosoftTeams";
7
+ import logger from "../Utils/Logger";
8
+ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
9
+ import WorkspaceProjectAuthToken from "../../Models/DatabaseModels/WorkspaceProjectAuthToken";
10
+ import BadDataException from "../../Types/Exception/BadDataException";
11
+ import ObjectID from "../../Types/ObjectID";
12
+ import UserNotificationStatus from "../../Types/UserNotification/UserNotificationStatus";
13
+ import WorkspaceNotificationActionType from "../../Types/Workspace/WorkspaceNotificationActionType";
14
+ import WorkspaceNotificationStatus from "../../Types/Workspace/WorkspaceNotificationStatus";
15
+ import { WorkspaceMessageBlock } from "../../Types/Workspace/WorkspaceMessagePayload";
16
+ import WorkspaceType, {
17
+ getWorkspaceTypeDisplayName,
18
+ } from "../../Types/Workspace/WorkspaceType";
19
+
20
+ /*
21
+ * Delivers a OneUptime notification to ONE person as a workspace direct
22
+ * message — the send half of the Slack / Microsoft Teams notification methods
23
+ * (UserSlack / UserMicrosoftTeams rows created under User Settings →
24
+ * Notification Methods).
25
+ *
26
+ * This is deliberately NOT part of the channel-oriented workspace machinery
27
+ * (WorkspaceNotificationRuleService and friends): those post into project
28
+ * channels selected by project-level rules, while this addresses a single
29
+ * user id captured from that user's own workspace link, using the project's
30
+ * bot credentials. SMS / Call / Telegram route through the Notification
31
+ * FeatureSet over HTTP because provider credentials and spend accounting live
32
+ * there; workspace bot tokens live in this database, so this service sends
33
+ * in-process instead.
34
+ */
35
+
36
+ export interface SendWorkspaceDirectMessageOptions {
37
+ projectId: ObjectID;
38
+ workspaceType: WorkspaceType;
39
+ /*
40
+ * Slack: the member id (U…). Microsoft Teams: the Microsoft Entra object
41
+ * id. Both are what the corresponding notification-method row stores.
42
+ */
43
+ workspaceUserId: string;
44
+ messageBlocks: Array<WorkspaceMessageBlock>;
45
+ /* Short plain-text form of the message, stored on the workspace log row. */
46
+ messageSummary?: string | undefined;
47
+ userId?: ObjectID | undefined;
48
+ /*
49
+ * When set, the on-call timeline row is flipped to Sent / Error after the
50
+ * send attempt, mirroring what the Notification FeatureSet does for the
51
+ * other channels.
52
+ */
53
+ userOnCallLogTimelineId?: ObjectID | undefined;
54
+ incidentId?: ObjectID | undefined;
55
+ alertId?: ObjectID | undefined;
56
+ alertEpisodeId?: ObjectID | undefined;
57
+ incidentEpisodeId?: ObjectID | undefined;
58
+ onCallPolicyId?: ObjectID | undefined;
59
+ onCallPolicyEscalationRuleId?: ObjectID | undefined;
60
+ onCallScheduleId?: ObjectID | undefined;
61
+ teamId?: ObjectID | undefined;
62
+ }
63
+
64
+ export class WorkspaceUserNotificationService extends BaseService {
65
+ public constructor() {
66
+ super();
67
+ }
68
+
69
+ @CaptureSpan()
70
+ public async sendDirectMessageToUser(
71
+ options: SendWorkspaceDirectMessageOptions,
72
+ ): Promise<void> {
73
+ const workspaceDisplayName: string = getWorkspaceTypeDisplayName(
74
+ options.workspaceType,
75
+ );
76
+
77
+ let sendError: Error | null = null;
78
+
79
+ try {
80
+ if (!options.workspaceUserId) {
81
+ throw new BadDataException(
82
+ `This account is not connected to ${workspaceDisplayName}. Please go to User Settings and connect the account.`,
83
+ );
84
+ }
85
+
86
+ const projectAuth: WorkspaceProjectAuthToken | null =
87
+ await WorkspaceProjectAuthTokenService.getProjectAuth({
88
+ projectId: options.projectId,
89
+ workspaceType: options.workspaceType,
90
+ });
91
+
92
+ if (!projectAuth || !projectAuth.authToken) {
93
+ throw new BadDataException(
94
+ `This project is not connected to ${workspaceDisplayName}. Please go to Project Settings and connect the account.`,
95
+ );
96
+ }
97
+
98
+ if (options.workspaceType === WorkspaceType.Slack) {
99
+ await SlackUtil.sendDirectMessageToUser({
100
+ authToken: projectAuth.authToken,
101
+ workspaceUserId: options.workspaceUserId,
102
+ messageBlocks: options.messageBlocks,
103
+ });
104
+ } else if (options.workspaceType === WorkspaceType.MicrosoftTeams) {
105
+ await MicrosoftTeamsUtil.sendDirectMessageToUserAsBot({
106
+ projectId: options.projectId,
107
+ workspaceUserId: options.workspaceUserId,
108
+ messageBlocks: options.messageBlocks,
109
+ });
110
+ } else {
111
+ throw new BadDataException(
112
+ `Direct messages are not supported for workspace type ${options.workspaceType}.`,
113
+ );
114
+ }
115
+ } catch (error: unknown) {
116
+ const errorMessage: string =
117
+ error instanceof Error && error.message
118
+ ? error.message
119
+ : `${error as string}`;
120
+
121
+ sendError = error instanceof Error ? error : new Error(errorMessage);
122
+
123
+ logger.error(
124
+ `Failed to send ${workspaceDisplayName} direct message notification.`,
125
+ );
126
+ logger.error(error);
127
+ }
128
+
129
+ /*
130
+ * Log the attempt for admin visibility. Best-effort: a failed log write
131
+ * must never turn a delivered page into a reported failure (or mask a
132
+ * real send error with a logging one).
133
+ */
134
+ try {
135
+ await WorkspaceNotificationLogService.createWorkspaceLog(
136
+ {
137
+ projectId: options.projectId,
138
+ workspaceType: options.workspaceType,
139
+ actionType: WorkspaceNotificationActionType.SendMessage,
140
+ status: sendError
141
+ ? WorkspaceNotificationStatus.Error
142
+ : WorkspaceNotificationStatus.Success,
143
+ message: options.messageSummary,
144
+ statusMessage: sendError
145
+ ? sendError.message
146
+ : `Direct message sent on ${workspaceDisplayName}.`,
147
+ userId: options.userId,
148
+ incidentId: options.incidentId,
149
+ alertId: options.alertId,
150
+ alertEpisodeId: options.alertEpisodeId,
151
+ incidentEpisodeId: options.incidentEpisodeId,
152
+ onCallDutyPolicyId: options.onCallPolicyId,
153
+ onCallDutyPolicyEscalationRuleId:
154
+ options.onCallPolicyEscalationRuleId,
155
+ onCallDutyPolicyScheduleId: options.onCallScheduleId,
156
+ teamId: options.teamId,
157
+ },
158
+ {
159
+ isRoot: true,
160
+ },
161
+ );
162
+ } catch (logError) {
163
+ logger.error("Failed to write workspace notification log.");
164
+ logger.error(logError);
165
+ }
166
+
167
+ if (options.userOnCallLogTimelineId) {
168
+ await UserOnCallLogTimelineService.updateOneById({
169
+ id: options.userOnCallLogTimelineId,
170
+ data: {
171
+ status: sendError
172
+ ? UserNotificationStatus.Error
173
+ : UserNotificationStatus.Sent,
174
+ statusMessage: sendError
175
+ ? sendError.message
176
+ : `Message sent on ${workspaceDisplayName}.`,
177
+ },
178
+ props: {
179
+ isRoot: true,
180
+ },
181
+ });
182
+ }
183
+
184
+ if (sendError) {
185
+ throw sendError;
186
+ }
187
+ }
188
+ }
189
+
190
+ export default new WorkspaceUserNotificationService();
@@ -0,0 +1,145 @@
1
+ import Attribution from "../Attribution";
2
+ import MarketingEventWebhook from "./MarketingEventWebhook";
3
+ import Queue, { QueueName } from "../../Infrastructure/Queue";
4
+ import { JSONObject } from "../../../Types/JSON";
5
+ import {
6
+ MARKETING_EVENT_SCHEMA_VERSION,
7
+ MarketingEvent,
8
+ MarketingEventAttribution,
9
+ MarketingEventType,
10
+ } from "../../../Types/Marketing/MarketingEvent";
11
+ import logger from "../Logger";
12
+
13
+ /*
14
+ * How many times the queue delivers one event before giving up, and the base
15
+ * for its exponential backoff. Five attempts at 30s exponential spans roughly
16
+ * eight minutes, which covers a rolling deploy of the receiver.
17
+ *
18
+ * There is no dead-letter store and deliberately so: the point of this design
19
+ * is that OneUptime does not hold marketing data. An event that exhausts its
20
+ * attempts is logged as an error and lost, which is the cost of not keeping a
21
+ * ledger and should be alerted on rather than engineered around here.
22
+ */
23
+ const DELIVERY_ATTEMPTS: number = 5;
24
+ const DELIVERY_BACKOFF_MS: number = 30_000;
25
+
26
+ /*
27
+ * Anything carrying the attribution columns. User and Project both do —
28
+ * ProjectService copies them off the creating user at project creation — so
29
+ * one shape serves every caller and they cannot drift apart.
30
+ */
31
+ export interface AttributionSource {
32
+ utmSource?: string | undefined;
33
+ utmMedium?: string | undefined;
34
+ utmCampaign?: string | undefined;
35
+ utmTerm?: string | undefined;
36
+ utmContent?: string | undefined;
37
+ utmUrl?: string | undefined;
38
+ clickIds?: JSONObject | undefined;
39
+ firstTouchAttribution?: JSONObject | undefined;
40
+ }
41
+
42
+ export default class MarketingEventUtil {
43
+ public static buildAttribution(
44
+ source: AttributionSource | undefined,
45
+ ): MarketingEventAttribution {
46
+ return {
47
+ utmSource: source?.utmSource,
48
+ utmMedium: source?.utmMedium,
49
+ utmCampaign: source?.utmCampaign,
50
+ utmTerm: source?.utmTerm,
51
+ utmContent: source?.utmContent,
52
+ utmUrl: source?.utmUrl,
53
+ clickIds: source?.clickIds || {},
54
+ firstTouch: source?.firstTouchAttribution || {},
55
+ };
56
+ }
57
+
58
+ public static buildEvent(data: {
59
+ eventType: MarketingEventType;
60
+ eventId: string;
61
+ occurredAt: Date;
62
+ email?: string | undefined;
63
+ attributionSource?: AttributionSource | undefined;
64
+ data?: JSONObject | undefined;
65
+ }): MarketingEvent {
66
+ const emailHash: string | null = Attribution.hashEmail(data.email);
67
+
68
+ return {
69
+ schemaVersion: MARKETING_EVENT_SCHEMA_VERSION,
70
+ eventId: data.eventId,
71
+ eventType: data.eventType,
72
+ occurredAt: data.occurredAt.toISOString(),
73
+ email: data.email,
74
+ emailHash: emailHash || undefined,
75
+ attribution: this.buildAttribution(data.attributionSource),
76
+ data: data.data || {},
77
+ };
78
+ }
79
+
80
+ /*
81
+ * Hand one event to the queue.
82
+ *
83
+ * Never throws and never awaits delivery. Every caller is a commercial code
84
+ * path — a signup completing, a plan change being written, a booking being
85
+ * accepted — and none of them may fail, block, or slow down because a
86
+ * marketing endpoint is unreachable. Enqueue is the only synchronous part
87
+ * and it is a single Redis write.
88
+ */
89
+ public static async emit(event: MarketingEvent): Promise<void> {
90
+ if (MarketingEventWebhook.isMisconfigured()) {
91
+ logger.error(
92
+ `MarketingEvent: MARKETING_WEBHOOK_URL is set but MARKETING_WEBHOOK_SECRET is not — refusing to send ${event.eventType} unsigned. Set the secret or unset the URL.`,
93
+ );
94
+ return;
95
+ }
96
+
97
+ if (!MarketingEventWebhook.isConfigured()) {
98
+ return;
99
+ }
100
+
101
+ try {
102
+ await Queue.addJob(
103
+ QueueName.MarketingEvent,
104
+ event.eventId,
105
+ event.eventType,
106
+ event as JSONObject,
107
+ {
108
+ attempts: DELIVERY_ATTEMPTS,
109
+ backoffDelayInMs: DELIVERY_BACKOFF_MS,
110
+ },
111
+ );
112
+
113
+ logger.debug(
114
+ `MarketingEvent: queued ${event.eventType} (${event.eventId})`,
115
+ );
116
+ } catch (err) {
117
+ logger.error(
118
+ `MarketingEvent: failed to queue ${event.eventType} (${event.eventId}): ${err}`,
119
+ );
120
+ }
121
+ }
122
+
123
+ /*
124
+ * Fire-and-forget wrapper for callers that are not async or must not await.
125
+ *
126
+ * Guarantees it neither throws nor rejects. Every caller is inside a
127
+ * commercial transaction that has already succeeded by the time it gets
128
+ * here — a user is created, a plan is changed, a booking is verified — so
129
+ * the one thing this must never do is turn a completed action into a failed
130
+ * one. The rejection path is covered by .catch and the synchronous path by
131
+ * the try, because an argument expression that throws (buildEvent on a
132
+ * malformed row) would otherwise propagate straight into the caller.
133
+ */
134
+ public static emitInBackground(event: MarketingEvent): void {
135
+ try {
136
+ this.emit(event).catch((err: Error) => {
137
+ logger.error(
138
+ `MarketingEvent: failed to emit ${event.eventType}: ${err}`,
139
+ );
140
+ });
141
+ } catch (err) {
142
+ logger.error(`MarketingEvent: failed to emit ${event.eventType}: ${err}`);
143
+ }
144
+ }
145
+ }
@@ -0,0 +1,114 @@
1
+ import axios, { AxiosError, AxiosResponse } from "axios";
2
+ import crypto from "crypto";
3
+ import {
4
+ MarketingWebhookSecret,
5
+ MarketingWebhookUrl,
6
+ } from "../../EnvironmentConfig";
7
+ import { MarketingEvent } from "../../../Types/Marketing/MarketingEvent";
8
+ import logger from "../Logger";
9
+
10
+ const REQUEST_TIMEOUT_MS: number = 15000;
11
+
12
+ /*
13
+ * Delivers marketing conversion events to one operator-configured endpoint.
14
+ *
15
+ * OneUptime keeps no conversion ledger, so this is the only exit for a signup,
16
+ * a booked meeting or a plan change. That has one consequence worth stating
17
+ * plainly: a delivery this class gives up on is gone, because there is no row
18
+ * anywhere to reconcile against later. deliver() therefore THROWS on anything
19
+ * that might succeed on a retry, and the queue is what retries it.
20
+ *
21
+ * SIGNING
22
+ *
23
+ * The signature is HMAC-SHA256 over the exact request body bytes, hex encoded,
24
+ * in `x-oneuptime-signature-256`. This is the same scheme OneUptime verifies
25
+ * on the way in from Cal, deliberately: one thing to understand rather than
26
+ * two. The receiver must compute its digest over the raw bytes it received —
27
+ * JSON parsed and re-serialised is NOT equivalent, because whitespace, key
28
+ * order and escaping all change the digest.
29
+ *
30
+ * The body is serialised once, here, and that exact string is both signed and
31
+ * sent, so nothing between the signature and the socket can reformat it.
32
+ *
33
+ * Both the URL and the secret are required. An endpoint configured without a
34
+ * secret is not sent to at all rather than sent to unsigned: the payload
35
+ * carries email addresses and campaign data, and a receiver with no way to
36
+ * tell OneUptime's POST from anyone else's is not a receiver worth having.
37
+ */
38
+ export default class MarketingEventWebhook {
39
+ public static isConfigured(): boolean {
40
+ return Boolean(MarketingWebhookUrl && MarketingWebhookSecret);
41
+ }
42
+
43
+ /*
44
+ * True when an endpoint is set but unusable, so callers can say so once at
45
+ * emit time rather than letting events vanish into a silent no-op.
46
+ */
47
+ public static isMisconfigured(): boolean {
48
+ return Boolean(MarketingWebhookUrl) && !MarketingWebhookSecret;
49
+ }
50
+
51
+ public static sign(body: string): string {
52
+ return crypto
53
+ .createHmac("sha256", MarketingWebhookSecret)
54
+ .update(body, "utf8")
55
+ .digest("hex");
56
+ }
57
+
58
+ /*
59
+ * One delivery attempt.
60
+ *
61
+ * Throws on a transport error or any non-2xx, which is what makes the queue
62
+ * retry. A 4xx is retried too: the receiver rejecting a payload it should
63
+ * have taken is far more often a deploy in progress or a bad rule than a
64
+ * payload that will never be acceptable, and the alternative — dropping it
65
+ * silently — has no backstop now that nothing is stored.
66
+ */
67
+ public static async deliver(event: MarketingEvent): Promise<void> {
68
+ if (!this.isConfigured()) {
69
+ return;
70
+ }
71
+
72
+ // Serialise once. This exact string is signed and sent.
73
+ const body: string = JSON.stringify(event);
74
+
75
+ try {
76
+ const response: AxiosResponse = await axios.post(
77
+ MarketingWebhookUrl,
78
+ body,
79
+ {
80
+ headers: {
81
+ "content-type": "application/json",
82
+ "x-oneuptime-signature-256": this.sign(body),
83
+ "x-oneuptime-event-id": event.eventId,
84
+ "x-oneuptime-event-type": event.eventType,
85
+ },
86
+ timeout: REQUEST_TIMEOUT_MS,
87
+ // Non-2xx must reach the catch below rather than resolving.
88
+ validateStatus: (status: number): boolean => {
89
+ return status >= 200 && status < 300;
90
+ },
91
+ },
92
+ );
93
+
94
+ logger.debug(
95
+ `MarketingEvent: delivered ${event.eventType} (${event.eventId}) — HTTP ${response.status}`,
96
+ );
97
+ } catch (err) {
98
+ const message: string =
99
+ err instanceof AxiosError
100
+ ? `HTTP ${err.response?.status || "?"}: ${JSON.stringify(
101
+ err.response?.data || err.message,
102
+ ).slice(0, 500)}`
103
+ : (err as Error)?.message || "Unknown error";
104
+
105
+ /*
106
+ * Rethrown, not swallowed. The queue's retry is the only thing standing
107
+ * between a receiver hiccup and a permanently lost conversion.
108
+ */
109
+ throw new Error(
110
+ `Marketing webhook delivery failed for ${event.eventType} (${event.eventId}): ${message}`,
111
+ );
112
+ }
113
+ }
114
+ }
@@ -7,6 +7,7 @@ import OneUptimeDate from "../../../Types/Date";
7
7
  import ObjectID from "../../../Types/ObjectID";
8
8
  import { JSONObject } from "../../../Types/JSON";
9
9
  import DataToProcess from "./DataToProcess";
10
+ import { redactForPersistence } from "./MonitorPayloadRedaction";
10
11
 
11
12
  /*
12
13
  * Maximum rows held in memory before we force a flush, and the
@@ -138,7 +139,19 @@ export default class MonitorLogUtil {
138
139
  projectId: data.projectId.toString(),
139
140
  monitorId: data.monitorId.toString(),
140
141
  time: logTimestamp,
141
- logBody: JSON.parse(JSON.stringify(data.dataToProcess)),
142
+ /*
143
+ * Redact on the way out, never in place: `dataToProcess` is still
144
+ * live here (saveMonitorLog is fire-and-forget and the caller keeps
145
+ * evaluating criteria against it), so the stringify/parse clone is
146
+ * what gets masked. logBody is readable by Permission.Viewer, which
147
+ * makes it the widest-reach sink in the product — anything
148
+ * credential-shaped in it is readable by the least-privileged role
149
+ * OneUptime grants. See MonitorPayloadRedaction.
150
+ * https://github.com/OneUptime/oneuptime/issues/3360
151
+ */
152
+ logBody: redactForPersistence(
153
+ JSON.parse(JSON.stringify(data.dataToProcess)),
154
+ ),
142
155
  retentionDate: OneUptimeDate.toClickhouseDateTime(retentionDate),
143
156
  };
144
157