@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.
- package/Models/DatabaseModels/EnterpriseLicense.ts +23 -0
- package/Models/DatabaseModels/Index.ts +4 -2
- package/Models/DatabaseModels/Monitor.ts +39 -9
- package/Models/DatabaseModels/UserMicrosoftTeams.ts +344 -0
- package/Models/DatabaseModels/UserNotificationRule.ts +144 -8
- package/Models/DatabaseModels/UserNotificationSetting.ts +34 -0
- package/Models/DatabaseModels/UserOnCallLogTimeline.ts +95 -0
- package/Models/DatabaseModels/UserSlack.ts +341 -0
- package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +9 -0
- package/Server/API/UserMicrosoftTeamsAPI.ts +127 -0
- package/Server/API/UserSlackAPI.ts +125 -0
- package/Server/EnvironmentConfig.ts +12 -107
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.ts +51 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +117 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +197 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Infrastructure/Queue.ts +7 -0
- package/Server/Services/EnterpriseLicenseService.ts +60 -0
- package/Server/Services/Index.ts +6 -2
- package/Server/Services/LlmLogService.ts +25 -9
- package/Server/Services/OnCallReadinessService.ts +86 -2
- package/Server/Services/ProjectService.ts +67 -0
- package/Server/Services/UserMicrosoftTeamsService.ts +208 -0
- package/Server/Services/UserNotificationMethodAdminService.ts +167 -5
- package/Server/Services/UserNotificationRuleAdminService.ts +40 -6
- package/Server/Services/UserNotificationRuleService.ts +589 -11
- package/Server/Services/UserNotificationSettingService.ts +138 -0
- package/Server/Services/UserService.ts +28 -0
- package/Server/Services/UserSlackService.ts +218 -0
- package/Server/Services/WorkspaceProjectAuthTokenService.ts +67 -1
- package/Server/Services/WorkspaceUserAuthTokenService.ts +73 -0
- package/Server/Services/WorkspaceUserNotificationService.ts +190 -0
- package/Server/Utils/Marketing/MarketingEventUtil.ts +145 -0
- package/Server/Utils/Marketing/MarketingEventWebhook.ts +114 -0
- package/Server/Utils/Monitor/MonitorLogUtil.ts +14 -1
- package/Server/Utils/Monitor/MonitorPayloadRedaction.ts +321 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +191 -4
- package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +22 -14
- package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +22 -11
- package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +37 -1
- package/Tests/App/Dashboard/OnCallRulesTable.test.tsx +18 -10
- package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +27 -1
- package/Tests/Models/DatabaseModels/MonitorSecretKeyColumnAccessControl.test.ts +214 -0
- package/Tests/Server/API/OnCallReadinessAPI.test.ts +145 -26
- package/Tests/Server/API/UserNotificationMethodAdminAPI.test.ts +2 -2
- package/Tests/Server/Services/AIServiceDailyBudget.test.ts +11 -1
- package/Tests/Server/Services/AdminRuleEditGuards.test.ts +22 -8
- package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +18 -2
- package/Tests/Server/Services/EpisodeRuleSeverityRepair.test.ts +5 -1
- package/Tests/Server/Services/LlmLogServiceTokenAggregateParams.test.ts +640 -0
- package/Tests/Server/Services/NotificationChannelEventCoverage.test.ts +246 -22
- package/Tests/Server/Services/NotificationDeletionImpact.test.ts +136 -8
- package/Tests/Server/Services/OnCallNotificationFallback.test.ts +191 -8
- package/Tests/Server/Services/OnCallReadinessService.test.ts +343 -18
- package/Tests/Server/Services/SeverityRuleBackfill.test.ts +5 -1
- package/Tests/Server/Services/UserMicrosoftTeamsService.test.ts +343 -0
- package/Tests/Server/Services/UserNotificationMethodAdminService.test.ts +209 -8
- package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +22 -6
- package/Tests/Server/Services/UserNotificationRuleDefaultCreation.test.ts +12 -6
- package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +37 -3
- package/Tests/Server/Services/UserNotificationRuleWorkspaceDelivery.test.ts +669 -0
- package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +410 -0
- package/Tests/Server/Services/UserSlackService.test.ts +407 -0
- package/Tests/Server/Services/WorkspaceProjectAuthTokenDisconnectCascade.test.ts +224 -0
- package/Tests/Server/Services/WorkspaceUserAuthTokenNotificationMethodCascade.test.ts +189 -0
- package/Tests/Server/Services/WorkspaceUserNotificationService.test.ts +496 -0
- package/Tests/Server/Types/Database/Permissions/AdminNotificationRuleAccess.test.ts +30 -17
- package/Tests/Server/Types/Database/Permissions/CreateOwnershipScoping.test.ts +8 -3
- package/Tests/Server/Types/Database/Permissions/OwnerOnlyColumns.test.ts +72 -3
- package/Tests/Server/Types/Database/Permissions/UserNotificationRuleScoping.test.ts +22 -18
- package/Tests/Server/Types/Database/Permissions/WorkspaceMethodStampedColumnCreate.test.ts +178 -0
- package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +259 -0
- package/Tests/Server/Utils/Marketing/MarketingEventWebhook.test.ts +257 -0
- package/Tests/Server/Utils/Monitor/MonitorLogUtilRedaction.test.ts +300 -0
- package/Tests/Server/Utils/Monitor/MonitorPayloadRedaction.test.ts +381 -0
- package/Tests/Server/Utils/Runbook/RunbookExecutePermission.test.ts +228 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayErasureTombstone.test.ts +189 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayUsage.test.ts +205 -0
- package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +275 -0
- package/Tests/Server/Utils/Telemetry/TelemetryContext.test.ts +279 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsDirectMessage.test.ts +454 -0
- package/Tests/UI/Utils/NotificationMethodUtil.test.ts +137 -9
- package/Tests/UI/Utils/PermissionGate.test.ts +105 -0
- package/Types/Marketing/MarketingEvent.ts +97 -0
- package/UI/Utils/NotificationMethodUtil.ts +41 -1
- package/UI/Utils/PermissionGate.ts +56 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +24 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +4 -2
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Monitor.js +39 -9
- package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js +363 -0
- package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js.map +1 -0
- package/build/dist/Models/DatabaseModels/UserNotificationRule.js +145 -8
- package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserNotificationSetting.js +38 -0
- package/build/dist/Models/DatabaseModels/UserNotificationSetting.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +96 -0
- package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserSlack.js +361 -0
- package/build/dist/Models/DatabaseModels/UserSlack.js.map +1 -0
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
- package/build/dist/Server/API/UserMicrosoftTeamsAPI.js +82 -0
- package/build/dist/Server/API/UserMicrosoftTeamsAPI.js.map +1 -0
- package/build/dist/Server/API/UserSlackAPI.js +81 -0
- package/build/dist/Server/API/UserSlackAPI.js.map +1 -0
- package/build/dist/Server/EnvironmentConfig.js +10 -70
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js +37 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +106 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +78 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Queue.js +7 -0
- package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
- package/build/dist/Server/Services/EnterpriseLicenseService.js +65 -0
- package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +6 -2
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/LlmLogService.js +25 -9
- package/build/dist/Server/Services/LlmLogService.js.map +1 -1
- package/build/dist/Server/Services/OnCallReadinessService.js +74 -2
- package/build/dist/Server/Services/OnCallReadinessService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +56 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/UserMicrosoftTeamsService.js +183 -0
- package/build/dist/Server/Services/UserMicrosoftTeamsService.js.map +1 -0
- package/build/dist/Server/Services/UserNotificationMethodAdminService.js +138 -2
- package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleAdminService.js +32 -4
- package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +499 -67
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js +118 -0
- package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +26 -1
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Services/UserSlackService.js +195 -0
- package/build/dist/Server/Services/UserSlackService.js.map +1 -0
- package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js +64 -1
- package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js +70 -0
- package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceUserNotificationService.js +129 -0
- package/build/dist/Server/Services/WorkspaceUserNotificationService.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +94 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +93 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js +12 -1
- package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js +233 -0
- package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +141 -4
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/Marketing/MarketingEvent.js +43 -0
- package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -0
- package/build/dist/UI/Utils/NotificationMethodUtil.js +33 -1
- package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -1
- package/build/dist/UI/Utils/PermissionGate.js +32 -0
- package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
- package/package.json +1 -1
- package/Models/DatabaseModels/MarketingConversion.ts +0 -410
- package/Server/Services/MarketingConversionService.ts +0 -10
- package/Server/Utils/Marketing/ConversionUploadProvider.ts +0 -215
- package/Server/Utils/Marketing/ConversionUploadProviders.ts +0 -22
- package/Server/Utils/Marketing/Providers/GoogleAds.ts +0 -333
- package/Server/Utils/Marketing/Providers/LinkedIn.ts +0 -185
- package/Server/Utils/Marketing/Providers/Meta.ts +0 -182
- package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +0 -223
- package/Server/Utils/Marketing/Providers/Reddit.ts +0 -208
- package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +0 -315
- package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +0 -300
- package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +0 -592
- package/Tests/Server/Utils/Marketing/LinkedIn.test.ts +0 -282
- package/Tests/Server/Utils/Marketing/Meta.test.ts +0 -304
- package/Tests/Server/Utils/Marketing/MicrosoftAds.test.ts +0 -263
- package/Tests/Server/Utils/Marketing/Reddit.test.ts +0 -259
- package/Types/Marketing/MarketingConversion.ts +0 -53
- package/build/dist/Models/DatabaseModels/MarketingConversion.js +0 -445
- package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +0 -1
- package/build/dist/Server/Services/MarketingConversionService.js +0 -9
- package/build/dist/Server/Services/MarketingConversionService.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +0 -93
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js +0 -20
- package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +0 -221
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +0 -137
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js +0 -133
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +0 -143
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +0 -145
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +0 -1
- package/build/dist/Types/Marketing/MarketingConversion.js +0 -52
- package/build/dist/Types/Marketing/MarketingConversion.js.map +0 -1
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
import axios, { AxiosResponse } from "axios";
|
|
2
|
-
import {
|
|
3
|
-
RedditAdsAccountId,
|
|
4
|
-
RedditAdsOAuthClientId,
|
|
5
|
-
RedditAdsOAuthClientSecret,
|
|
6
|
-
RedditAdsOAuthRefreshToken,
|
|
7
|
-
} from "../../../EnvironmentConfig";
|
|
8
|
-
import ConversionUploadProvider, {
|
|
9
|
-
ConversionSkip,
|
|
10
|
-
ConversionTypeMapping,
|
|
11
|
-
ConversionUploadBatchResult,
|
|
12
|
-
} from "../ConversionUploadProvider";
|
|
13
|
-
import { JSONObject } from "../../../../Types/JSON";
|
|
14
|
-
import { MarketingConversionType } from "../../../../Types/Marketing/MarketingConversion";
|
|
15
|
-
import MarketingConversion from "../../../../Models/DatabaseModels/MarketingConversion";
|
|
16
|
-
|
|
17
|
-
const REQUEST_TIMEOUT_MS: number = 30000;
|
|
18
|
-
|
|
19
|
-
// Reddit accepts conversion events up to 7 days old.
|
|
20
|
-
const REDDIT_MAX_EVENT_AGE_IN_DAYS: number = 7;
|
|
21
|
-
|
|
22
|
-
/*
|
|
23
|
-
* Reddit Conversions API
|
|
24
|
-
* (ads-api.reddit.com/api/v2.0/conversions/events/{accountId}) using the
|
|
25
|
-
* rdt_cid click id or a SHA-256 email, whichever the conversion carries.
|
|
26
|
-
* conversion_id (the conversion row id) makes retried uploads idempotent on
|
|
27
|
-
* Reddit's side. OAuth2 refresh-token flow with HTTP basic auth against
|
|
28
|
-
* reddit.com.
|
|
29
|
-
*/
|
|
30
|
-
export default class RedditProvider extends ConversionUploadProvider {
|
|
31
|
-
public override readonly key: string = "reddit";
|
|
32
|
-
public override readonly displayName: string = "Reddit";
|
|
33
|
-
|
|
34
|
-
private cachedAccessToken: string | null = null;
|
|
35
|
-
private cachedAccessTokenExpiresAt: number = 0;
|
|
36
|
-
|
|
37
|
-
public override isConfigured(): boolean {
|
|
38
|
-
return Boolean(
|
|
39
|
-
RedditAdsOAuthClientId &&
|
|
40
|
-
RedditAdsOAuthClientSecret &&
|
|
41
|
-
RedditAdsOAuthRefreshToken &&
|
|
42
|
-
RedditAdsAccountId,
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
protected override getProviderSkipReason(
|
|
47
|
-
conversion: MarketingConversion,
|
|
48
|
-
): ConversionSkip | null {
|
|
49
|
-
if (
|
|
50
|
-
!this.getClickId(conversion, "rdt_cid") &&
|
|
51
|
-
!this.getHashedEmail(conversion)
|
|
52
|
-
) {
|
|
53
|
-
return {
|
|
54
|
-
reason: "No Reddit click id (rdt_cid) and no email to match on",
|
|
55
|
-
isPermanent: true,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (!this.getTrackingType(conversion)) {
|
|
60
|
-
return {
|
|
61
|
-
reason: "No Reddit tracking type mapped for this conversion type",
|
|
62
|
-
isPermanent: true,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (this.isOlderThanDays(conversion, REDDIT_MAX_EVENT_AGE_IN_DAYS)) {
|
|
67
|
-
return {
|
|
68
|
-
reason: "Conversion older than Reddit's 7-day upload window",
|
|
69
|
-
isPermanent: true,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/*
|
|
77
|
-
* Reddit standard tracking types. A booked meeting is `Lead`: Reddit has no
|
|
78
|
-
* distinct "meeting booked" type, and reporting it as Purchase would put a
|
|
79
|
-
* demo into the same optimisation pool as revenue.
|
|
80
|
-
*/
|
|
81
|
-
private getTrackingType(conversion: MarketingConversion): string | undefined {
|
|
82
|
-
const mapping: ConversionTypeMapping<string> = {
|
|
83
|
-
[MarketingConversionType.SignUp]: "SignUp",
|
|
84
|
-
[MarketingConversionType.MeetingBooked]: "Lead",
|
|
85
|
-
[MarketingConversionType.PaidSubscription]: "Purchase",
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
return this.resolveByConversionType(conversion, mapping);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
private async getAccessToken(): Promise<string> {
|
|
92
|
-
if (
|
|
93
|
-
this.cachedAccessToken &&
|
|
94
|
-
Date.now() < this.cachedAccessTokenExpiresAt
|
|
95
|
-
) {
|
|
96
|
-
return this.cachedAccessToken;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const params: URLSearchParams = new URLSearchParams();
|
|
100
|
-
params.append("grant_type", "refresh_token");
|
|
101
|
-
params.append("refresh_token", RedditAdsOAuthRefreshToken);
|
|
102
|
-
|
|
103
|
-
const response: AxiosResponse<{
|
|
104
|
-
access_token?: string;
|
|
105
|
-
expires_in?: number;
|
|
106
|
-
}> = await axios.post(
|
|
107
|
-
"https://www.reddit.com/api/v1/access_token",
|
|
108
|
-
params.toString(),
|
|
109
|
-
{
|
|
110
|
-
auth: {
|
|
111
|
-
username: RedditAdsOAuthClientId,
|
|
112
|
-
password: RedditAdsOAuthClientSecret,
|
|
113
|
-
},
|
|
114
|
-
headers: {
|
|
115
|
-
"content-type": "application/x-www-form-urlencoded",
|
|
116
|
-
// Reddit throttles/blocks requests without a descriptive User-Agent.
|
|
117
|
-
"User-Agent": "OneUptime-ConversionsUpload/1.0",
|
|
118
|
-
},
|
|
119
|
-
timeout: REQUEST_TIMEOUT_MS,
|
|
120
|
-
},
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
const accessToken: string | undefined = response.data?.access_token;
|
|
124
|
-
|
|
125
|
-
if (!accessToken) {
|
|
126
|
-
throw new Error("Reddit OAuth token response had no access_token");
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
this.cachedAccessToken = accessToken;
|
|
130
|
-
this.cachedAccessTokenExpiresAt =
|
|
131
|
-
Date.now() + ((response.data?.expires_in || 3600) - 60) * 1000;
|
|
132
|
-
|
|
133
|
-
return accessToken;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
public override async upload(
|
|
137
|
-
conversions: Array<MarketingConversion>,
|
|
138
|
-
): Promise<ConversionUploadBatchResult> {
|
|
139
|
-
const accessToken: string = await this.getAccessToken();
|
|
140
|
-
|
|
141
|
-
const events: Array<JSONObject> = conversions.map(
|
|
142
|
-
(conversion: MarketingConversion) => {
|
|
143
|
-
const eventAt: Date = conversion.conversionAt || new Date();
|
|
144
|
-
|
|
145
|
-
const eventMetadata: JSONObject = {
|
|
146
|
-
// Dedup key: retried uploads are idempotent on Reddit's side.
|
|
147
|
-
conversion_id: conversion.id!.toString(),
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
const valueInUSD: number | undefined = this.getValueInUSD(conversion);
|
|
151
|
-
|
|
152
|
-
if (valueInUSD !== undefined) {
|
|
153
|
-
eventMetadata["value_decimal"] = valueInUSD;
|
|
154
|
-
eventMetadata["currency"] = "USD";
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const payload: JSONObject = {
|
|
158
|
-
event_at: eventAt.toISOString(),
|
|
159
|
-
event_type: {
|
|
160
|
-
tracking_type: this.getTrackingType(conversion) || "Lead",
|
|
161
|
-
},
|
|
162
|
-
event_metadata: eventMetadata,
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
const clickId: string | undefined = this.getClickId(
|
|
166
|
-
conversion,
|
|
167
|
-
"rdt_cid",
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
if (clickId) {
|
|
171
|
-
payload["click_id"] = clickId;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const hashedEmail: string | undefined = this.getHashedEmail(conversion);
|
|
175
|
-
|
|
176
|
-
if (hashedEmail) {
|
|
177
|
-
payload["user"] = {
|
|
178
|
-
email: hashedEmail,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return payload;
|
|
183
|
-
},
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
await axios.post(
|
|
187
|
-
`https://ads-api.reddit.com/api/v2.0/conversions/events/${RedditAdsAccountId}`,
|
|
188
|
-
{
|
|
189
|
-
test_mode: false,
|
|
190
|
-
events: events,
|
|
191
|
-
},
|
|
192
|
-
{
|
|
193
|
-
headers: {
|
|
194
|
-
Authorization: `Bearer ${accessToken}`,
|
|
195
|
-
"Content-Type": "application/json",
|
|
196
|
-
},
|
|
197
|
-
timeout: REQUEST_TIMEOUT_MS,
|
|
198
|
-
},
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
/*
|
|
202
|
-
* Reddit accepts or rejects the request as a whole; request-level
|
|
203
|
-
* failures throw and the batch is retried (idempotent via
|
|
204
|
-
* conversion_id).
|
|
205
|
-
*/
|
|
206
|
-
return { permanentFailures: new Map<number, string>() };
|
|
207
|
-
}
|
|
208
|
-
}
|
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
import MarketingConversion from "../../../../Models/DatabaseModels/MarketingConversion";
|
|
2
|
-
import ConversionUploadProvider, {
|
|
3
|
-
ConversionSkip,
|
|
4
|
-
ConversionUploadBatchResult,
|
|
5
|
-
} from "../../../../Server/Utils/Marketing/ConversionUploadProvider";
|
|
6
|
-
import GoogleAdsProvider from "../../../../Server/Utils/Marketing/Providers/GoogleAds";
|
|
7
|
-
import LinkedInProvider from "../../../../Server/Utils/Marketing/Providers/LinkedIn";
|
|
8
|
-
import MetaProvider from "../../../../Server/Utils/Marketing/Providers/Meta";
|
|
9
|
-
import MicrosoftAdsProvider from "../../../../Server/Utils/Marketing/Providers/MicrosoftAds";
|
|
10
|
-
import RedditAdsProvider from "../../../../Server/Utils/Marketing/Providers/Reddit";
|
|
11
|
-
import {
|
|
12
|
-
AdUploadableMarketingConversionTypes,
|
|
13
|
-
LeadMarketingConversionTypes,
|
|
14
|
-
MarketingConversionType,
|
|
15
|
-
} from "../../../../Types/Marketing/MarketingConversion";
|
|
16
|
-
import { beforeEach, describe, expect, test } from "@jest/globals";
|
|
17
|
-
|
|
18
|
-
/*
|
|
19
|
-
* ---------------------------------------------------------------------------
|
|
20
|
-
* Which conversion types may be uploaded to an ad platform, and as what.
|
|
21
|
-
*
|
|
22
|
-
* THE HAZARD THIS GUARDS
|
|
23
|
-
*
|
|
24
|
-
* Providers used to choose their platform conversion action with a two-way
|
|
25
|
-
* branch:
|
|
26
|
-
*
|
|
27
|
-
* this.isSignUp(conversion) ? signUpAction : paidSubscriptionAction
|
|
28
|
-
*
|
|
29
|
-
* There was no third arm, so a conversion type nobody had written a mapping
|
|
30
|
-
* for did not get skipped by that branch — it was uploaded to Google, Meta,
|
|
31
|
-
* Microsoft, LinkedIn and Reddit as a PURCHASE carrying whatever revenue the
|
|
32
|
-
* row happened to hold. The old defence was to keep such types out of the
|
|
33
|
-
* uploadable allowlist entirely, which kept sales-led conversions out of the
|
|
34
|
-
* ad platforms altogether.
|
|
35
|
-
*
|
|
36
|
-
* They are in the allowlist now, and the defence is structural instead: every
|
|
37
|
-
* provider resolves its action through a Record over the conversion-type enum,
|
|
38
|
-
* which the compiler will not accept unless every member is named. What is
|
|
39
|
-
* left for these tests is everything the type system cannot see:
|
|
40
|
-
*
|
|
41
|
-
* - the column is a plain varchar, so a value that is not a known type at
|
|
42
|
-
* all must still be refused, permanently, before any provider sees it;
|
|
43
|
-
* - an unconfigured mapping must leave the row PENDING, not discard it —
|
|
44
|
-
* the difference between "we have not set this up yet" and "this can never
|
|
45
|
-
* be uploaded";
|
|
46
|
-
* - a lead (a booked meeting, a licence request) must never carry revenue to
|
|
47
|
-
* any platform, whatever the row says, or bid models optimise towards
|
|
48
|
-
* demos that buy nothing.
|
|
49
|
-
* ---------------------------------------------------------------------------
|
|
50
|
-
*/
|
|
51
|
-
|
|
52
|
-
class TestProvider extends ConversionUploadProvider {
|
|
53
|
-
public override readonly key: string = "test";
|
|
54
|
-
public override readonly displayName: string = "Test Provider";
|
|
55
|
-
public providerSkipCalls: number = 0;
|
|
56
|
-
public providerSkip: ConversionSkip | null = null;
|
|
57
|
-
|
|
58
|
-
public override isConfigured(): boolean {
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
protected override getProviderSkipReason(
|
|
63
|
-
_conversion: MarketingConversion,
|
|
64
|
-
): ConversionSkip | null {
|
|
65
|
-
this.providerSkipCalls++;
|
|
66
|
-
return this.providerSkip;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
public override async upload(
|
|
70
|
-
_conversions: Array<MarketingConversion>,
|
|
71
|
-
): Promise<ConversionUploadBatchResult> {
|
|
72
|
-
return { permanentFailures: new Map<number, string>() };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
public readValueInUSD(conversion: MarketingConversion): number | undefined {
|
|
76
|
-
return this.getValueInUSD(conversion);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
type MakeConversionFunction = (
|
|
81
|
-
data?: Partial<MarketingConversion>,
|
|
82
|
-
) => MarketingConversion;
|
|
83
|
-
|
|
84
|
-
const makeConversion: MakeConversionFunction = (
|
|
85
|
-
data: Partial<MarketingConversion> = {},
|
|
86
|
-
): MarketingConversion => {
|
|
87
|
-
return Object.assign(new MarketingConversion(), {
|
|
88
|
-
conversionType: MarketingConversionType.SignUp,
|
|
89
|
-
conversionAt: new Date("2026-08-19T10:00:00.000Z"),
|
|
90
|
-
// One click id per platform, so nothing is skipped for lack of one.
|
|
91
|
-
clickIds: {
|
|
92
|
-
gclid: "google-click",
|
|
93
|
-
fbclid: "meta-click",
|
|
94
|
-
msclkid: "microsoft-click",
|
|
95
|
-
li_fat_id: "linkedin-click",
|
|
96
|
-
rdt_cid: "reddit-click",
|
|
97
|
-
},
|
|
98
|
-
...data,
|
|
99
|
-
});
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
describe("ad-uploadable conversion types", () => {
|
|
103
|
-
describe("the allowlist itself", () => {
|
|
104
|
-
/*
|
|
105
|
-
* Every type is uploadable now, which is only safe because every provider
|
|
106
|
-
* maps every type. If a type is ever removed from this list again, that is
|
|
107
|
-
* a deliberate "no platform should hear about this" decision and should be
|
|
108
|
-
* spelled out here.
|
|
109
|
-
*/
|
|
110
|
-
test("covers every conversion type the ledger records", () => {
|
|
111
|
-
expect([...AdUploadableMarketingConversionTypes].sort()).toEqual(
|
|
112
|
-
[...Object.values(MarketingConversionType)].sort(),
|
|
113
|
-
);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
/*
|
|
117
|
-
* A booked meeting is the ONLY sales-led conversion. Asking about an
|
|
118
|
-
* enterprise licence and booking an architecture assessment are the same
|
|
119
|
-
* conversation reached through the same Cal embed, so they are the same
|
|
120
|
-
* conversion — not two types that would split the signal in reporting and
|
|
121
|
-
* need separate conversion actions on five platforms.
|
|
122
|
-
*/
|
|
123
|
-
test("classifies a booked meeting as the only sales-led step", () => {
|
|
124
|
-
expect(LeadMarketingConversionTypes).toEqual([
|
|
125
|
-
MarketingConversionType.MeetingBooked,
|
|
126
|
-
]);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test("never classifies a completed transaction as a lead", () => {
|
|
130
|
-
expect(LeadMarketingConversionTypes).not.toContain(
|
|
131
|
-
MarketingConversionType.SignUp,
|
|
132
|
-
);
|
|
133
|
-
expect(LeadMarketingConversionTypes).not.toContain(
|
|
134
|
-
MarketingConversionType.PaidSubscription,
|
|
135
|
-
);
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
describe("the base class screen", () => {
|
|
140
|
-
let provider: TestProvider;
|
|
141
|
-
|
|
142
|
-
beforeEach(() => {
|
|
143
|
-
provider = new TestProvider();
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test.each([
|
|
147
|
-
["an unknown type", "SomethingElse"],
|
|
148
|
-
["an empty type", ""],
|
|
149
|
-
["a lookalike with different casing", "meetingbooked"],
|
|
150
|
-
["a lookalike with different casing", "signup"],
|
|
151
|
-
["a lookalike with different casing", "enterpriselicenserequested"],
|
|
152
|
-
])("permanently skips %s", (_label: string, conversionType: string) => {
|
|
153
|
-
const skip: ConversionSkip | null = provider.getSkipReason(
|
|
154
|
-
makeConversion({ conversionType: conversionType }),
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
expect(skip?.isPermanent).toBe(true);
|
|
158
|
-
// The provider is never consulted about a type the ledger cannot name.
|
|
159
|
-
expect(provider.providerSkipCalls).toBe(0);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test("names an absent conversion type as unknown rather than blank", () => {
|
|
163
|
-
const conversion: MarketingConversion = makeConversion();
|
|
164
|
-
delete conversion.conversionType;
|
|
165
|
-
|
|
166
|
-
expect(provider.getSkipReason(conversion)).toEqual({
|
|
167
|
-
reason: "Conversion type unknown has no ad platform conversion mapping",
|
|
168
|
-
isPermanent: true,
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test.each(Object.values(MarketingConversionType))(
|
|
173
|
-
"delegates %s to the provider",
|
|
174
|
-
(conversionType: MarketingConversionType) => {
|
|
175
|
-
expect(
|
|
176
|
-
provider.getSkipReason(
|
|
177
|
-
makeConversion({ conversionType: conversionType }),
|
|
178
|
-
),
|
|
179
|
-
).toBeNull();
|
|
180
|
-
expect(provider.providerSkipCalls).toBe(1);
|
|
181
|
-
},
|
|
182
|
-
);
|
|
183
|
-
|
|
184
|
-
test("returns the provider's own skip verbatim for a mapped type", () => {
|
|
185
|
-
provider.providerSkip = {
|
|
186
|
-
reason: "No Google click id",
|
|
187
|
-
isPermanent: true,
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
expect(provider.getSkipReason(makeConversion())).toEqual({
|
|
191
|
-
reason: "No Google click id",
|
|
192
|
-
isPermanent: true,
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
describe("lead conversions never carry revenue", () => {
|
|
198
|
-
const provider: TestProvider = new TestProvider();
|
|
199
|
-
|
|
200
|
-
test.each(LeadMarketingConversionTypes)(
|
|
201
|
-
"%s reports no value even when the row holds one",
|
|
202
|
-
(conversionType: MarketingConversionType) => {
|
|
203
|
-
/*
|
|
204
|
-
* A row can legitimately hold a value here — nothing stops an operator
|
|
205
|
-
* assigning an estimated pipeline value to a booked meeting — and the
|
|
206
|
-
* platform still must not be told it as revenue.
|
|
207
|
-
*/
|
|
208
|
-
expect(
|
|
209
|
-
provider.readValueInUSD(
|
|
210
|
-
makeConversion({
|
|
211
|
-
conversionType: conversionType,
|
|
212
|
-
conversionValueInUSDCents: 500000,
|
|
213
|
-
}),
|
|
214
|
-
),
|
|
215
|
-
).toBeUndefined();
|
|
216
|
-
},
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
test("a paid subscription still reports its value", () => {
|
|
220
|
-
expect(
|
|
221
|
-
provider.readValueInUSD(
|
|
222
|
-
makeConversion({
|
|
223
|
-
conversionType: MarketingConversionType.PaidSubscription,
|
|
224
|
-
conversionValueInUSDCents: 500000,
|
|
225
|
-
}),
|
|
226
|
-
),
|
|
227
|
-
).toBe(5000);
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
/*
|
|
232
|
-
* Exercised through the real providers too: a structural guarantee in the
|
|
233
|
-
* base class is only worth something if no provider has quietly overridden
|
|
234
|
-
* getSkipReason back.
|
|
235
|
-
*
|
|
236
|
-
* These run with no ad-platform environment configured, which is the state a
|
|
237
|
-
* fresh checkout and CI are in — so what they assert is the behaviour of an
|
|
238
|
-
* UNCONFIGURED deployment, which is where the dangerous failure mode lives:
|
|
239
|
-
* a row silently thrown away is unrecoverable, a row left pending is not.
|
|
240
|
-
*/
|
|
241
|
-
describe("every shipped provider, unconfigured", () => {
|
|
242
|
-
const providers: Array<[string, ConversionUploadProvider]> = [
|
|
243
|
-
["Google Ads", new GoogleAdsProvider()],
|
|
244
|
-
["Meta", new MetaProvider()],
|
|
245
|
-
["Microsoft Ads", new MicrosoftAdsProvider()],
|
|
246
|
-
["LinkedIn", new LinkedInProvider()],
|
|
247
|
-
["Reddit Ads", new RedditAdsProvider()],
|
|
248
|
-
];
|
|
249
|
-
|
|
250
|
-
test.each(providers)(
|
|
251
|
-
"%s still permanently refuses an unknown conversion type",
|
|
252
|
-
(_name: string, provider: ConversionUploadProvider) => {
|
|
253
|
-
expect(
|
|
254
|
-
provider.getSkipReason(
|
|
255
|
-
makeConversion({ conversionType: "NotARealType" }),
|
|
256
|
-
),
|
|
257
|
-
).toEqual({
|
|
258
|
-
reason:
|
|
259
|
-
"Conversion type NotARealType has no ad platform conversion mapping",
|
|
260
|
-
isPermanent: true,
|
|
261
|
-
});
|
|
262
|
-
},
|
|
263
|
-
);
|
|
264
|
-
|
|
265
|
-
/*
|
|
266
|
-
* Google, Microsoft and LinkedIn each need an operator-supplied conversion
|
|
267
|
-
* action per type. Unset, that is a config gap: the row waits.
|
|
268
|
-
*/
|
|
269
|
-
const configuredByOperator: Array<[string, ConversionUploadProvider]> = [
|
|
270
|
-
["Google Ads", new GoogleAdsProvider()],
|
|
271
|
-
["Microsoft Ads", new MicrosoftAdsProvider()],
|
|
272
|
-
["LinkedIn", new LinkedInProvider()],
|
|
273
|
-
];
|
|
274
|
-
|
|
275
|
-
test.each(configuredByOperator)(
|
|
276
|
-
"%s leaves a booked meeting pending rather than discarding it",
|
|
277
|
-
(_name: string, provider: ConversionUploadProvider) => {
|
|
278
|
-
const skip: ConversionSkip | null = provider.getSkipReason(
|
|
279
|
-
makeConversion({
|
|
280
|
-
conversionType: MarketingConversionType.MeetingBooked,
|
|
281
|
-
}),
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
expect(skip).not.toBeNull();
|
|
285
|
-
expect(skip?.isPermanent).toBe(false);
|
|
286
|
-
expect(skip?.reason).toMatch(/conversion (action id|name|rule id)/i);
|
|
287
|
-
},
|
|
288
|
-
);
|
|
289
|
-
|
|
290
|
-
/*
|
|
291
|
-
* Meta and Reddit name their events with platform constants rather than an
|
|
292
|
-
* operator-configured id, so there is nothing to be missing: a booked
|
|
293
|
-
* meeting is uploadable as soon as it has an identifier.
|
|
294
|
-
*/
|
|
295
|
-
const selfDescribing: Array<[string, ConversionUploadProvider]> = [
|
|
296
|
-
["Meta", new MetaProvider()],
|
|
297
|
-
["Reddit Ads", new RedditAdsProvider()],
|
|
298
|
-
];
|
|
299
|
-
|
|
300
|
-
test.each(selfDescribing)(
|
|
301
|
-
"%s accepts a booked meeting outright",
|
|
302
|
-
(_name: string, provider: ConversionUploadProvider) => {
|
|
303
|
-
expect(
|
|
304
|
-
provider.getSkipReason(
|
|
305
|
-
makeConversion({
|
|
306
|
-
conversionType: MarketingConversionType.MeetingBooked,
|
|
307
|
-
// Inside Meta's and Reddit's 7-day window.
|
|
308
|
-
conversionAt: new Date(),
|
|
309
|
-
}),
|
|
310
|
-
),
|
|
311
|
-
).toBeNull();
|
|
312
|
-
},
|
|
313
|
-
);
|
|
314
|
-
});
|
|
315
|
-
});
|