@canonry/canonry 4.169.0 → 4.171.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/assets/{AuditHistoryPanel-B3APGWB4.js → AuditHistoryPanel-BpIn6E7p.js} +1 -1
- package/assets/assets/{BacklinksPage-C-7KAx5g.js → BacklinksPage-BvOWgx1l.js} +1 -1
- package/assets/assets/{HistoryPage-Cf-dJuPA.js → HistoryPage-BXaQyur8.js} +1 -1
- package/assets/assets/{MeasurementPropertyPage-C1W-BG81.js → MeasurementPropertyPage-Buj708lc.js} +1 -1
- package/assets/assets/{ProjectPage-CnZC8AA7.js → ProjectPage-BwC29w2_.js} +1 -1
- package/assets/assets/{RunRow-DJQ4Up9f.js → RunRow-C9hp5yEp.js} +1 -1
- package/assets/assets/{RunsPage-BbtbYLgP.js → RunsPage-DmkRH1iu.js} +1 -1
- package/assets/assets/{SettingsPage-DG-6IuO1.js → SettingsPage-BIyuj-gL.js} +1 -1
- package/assets/assets/{SiteHealthSection-rCrAk98W.js → SiteHealthSection-ZlYR3JtG.js} +3 -3
- package/assets/assets/{TrafficPage-CpS4K8eT.js → TrafficPage-PxIetTWH.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-D_zPmdUz.js → TrafficSourceDetailPage-BhF_leby.js} +1 -1
- package/assets/assets/{extract-error-message-40mUXcbq.js → extract-error-message-Dd2su-BK.js} +1 -1
- package/assets/assets/{index-Nk2MzD2d.js → index-DyjfMyum.js} +21 -21
- package/assets/assets/{react-sigma_core.esm.min-BOZ1RPZp.js → react-sigma_core.esm.min-CsNemdas.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-T2UJAYTX.js → chunk-5KGN224W.js} +3407 -137
- package/dist/{chunk-K7XXDKYY.js → chunk-HJ7W2LXI.js} +165 -1
- package/dist/{chunk-HVBAGSQC.js → chunk-IIFDS3K2.js} +807 -13
- package/dist/{chunk-U2X42SKN.js → chunk-O226BLC7.js} +3893 -1157
- package/dist/{chunk-ANA3GJSC.js → chunk-RR4KSDOD.js} +2 -2
- package/dist/cli.js +690 -111
- package/dist/index.d.ts +132 -2
- package/dist/index.js +10 -4
- package/dist/{intelligence-service-6ZYTEFXX.js → intelligence-service-KT6K4ODY.js} +2 -2
- package/dist/mcp.js +37 -4
- package/package.json +8 -8
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
loadConfig,
|
|
10
10
|
loadConfigRaw,
|
|
11
11
|
saveConfigPatch
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-IIFDS3K2.js";
|
|
13
13
|
import {
|
|
14
14
|
CC_CACHE_DIR,
|
|
15
15
|
DUCKDB_SPEC,
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
anyUsersExist,
|
|
31
31
|
apiKeys,
|
|
32
32
|
apiRoutes,
|
|
33
|
+
assessConversionTrackingIntegrity,
|
|
33
34
|
auditLog,
|
|
34
35
|
backlinkDomains,
|
|
35
36
|
backlinkSummaries,
|
|
@@ -55,6 +56,7 @@ import {
|
|
|
55
56
|
downloadFile,
|
|
56
57
|
dropLegacyCredentialColumns,
|
|
57
58
|
ensureCurrentQueryBasketRevision,
|
|
59
|
+
exchangeCode,
|
|
58
60
|
executeDiscovery,
|
|
59
61
|
extractLegacyCredentials,
|
|
60
62
|
extractRecommendedCompetitors,
|
|
@@ -72,10 +74,13 @@ import {
|
|
|
72
74
|
gbpPlaceActions,
|
|
73
75
|
gbpPlaceDetails,
|
|
74
76
|
getAttributes,
|
|
77
|
+
getAuthUrl,
|
|
75
78
|
getCrawlIssues,
|
|
76
79
|
getLodging,
|
|
77
80
|
getPlaceDetails,
|
|
78
81
|
getUrlInfo,
|
|
82
|
+
googleAdsConnections,
|
|
83
|
+
googleAdsRawSnapshots,
|
|
79
84
|
groupRunsByCreatedAt,
|
|
80
85
|
gscCoverageSnapshots,
|
|
81
86
|
gscDailyTotals,
|
|
@@ -83,6 +88,8 @@ import {
|
|
|
83
88
|
gscQueryDailyTotals,
|
|
84
89
|
gscSearchData,
|
|
85
90
|
gscUrlInspections,
|
|
91
|
+
gtmConnections,
|
|
92
|
+
gtmRawSnapshots,
|
|
86
93
|
hashAttributes,
|
|
87
94
|
hashLodging,
|
|
88
95
|
hashPlaceDetails,
|
|
@@ -138,7 +145,7 @@ import {
|
|
|
138
145
|
siteCrawlSnapshots,
|
|
139
146
|
toAlertView,
|
|
140
147
|
usageCounters
|
|
141
|
-
} from "./chunk-
|
|
148
|
+
} from "./chunk-O226BLC7.js";
|
|
142
149
|
import {
|
|
143
150
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
144
151
|
AGENT_PROVIDER_IDS,
|
|
@@ -151,6 +158,16 @@ import {
|
|
|
151
158
|
CITED_URL_CAPTURE_VERSION,
|
|
152
159
|
CcReleaseSyncStatuses,
|
|
153
160
|
DiscoveryCompetitorTypes,
|
|
161
|
+
GOOGLE_ADS_CAMPAIGN_METRICS_MAX_CAMPAIGNS,
|
|
162
|
+
GOOGLE_ADS_CAMPAIGN_METRICS_MAX_DAYS,
|
|
163
|
+
GOOGLE_ADS_CAMPAIGN_METRICS_MAX_ROWS,
|
|
164
|
+
GoogleAdsCampaignStatuses,
|
|
165
|
+
GoogleAdsConversionActionStatuses,
|
|
166
|
+
GoogleAdsCustomerStatuses,
|
|
167
|
+
GoogleAdsGoalConfigurationLevels,
|
|
168
|
+
GoogleAdsSnapshotKinds,
|
|
169
|
+
GoogleMarketingProviders,
|
|
170
|
+
GtmSnapshotKinds,
|
|
154
171
|
LLM_CAPABILITIES,
|
|
155
172
|
LlmCapabilities,
|
|
156
173
|
MemorySources,
|
|
@@ -188,6 +205,9 @@ import {
|
|
|
188
205
|
authRequired,
|
|
189
206
|
bucketOnboardingCount,
|
|
190
207
|
buildRunErrorFromMessages,
|
|
208
|
+
canonicalizeGtmAccountId,
|
|
209
|
+
canonicalizeGtmContainerId,
|
|
210
|
+
canonicalizeGtmResourceSelection,
|
|
191
211
|
classifyTemplateLinkEdge,
|
|
192
212
|
cloudflareQueueNameSchema,
|
|
193
213
|
computeBacklinkSummaryMetrics,
|
|
@@ -196,6 +216,7 @@ import {
|
|
|
196
216
|
createTemplateLinkDetectionTally,
|
|
197
217
|
createTemplateLinkPairIndex,
|
|
198
218
|
deriveCitedUrlCandidates,
|
|
219
|
+
deriveGoogleAdsEffectiveGoalGraph,
|
|
199
220
|
deriveIndexCoverage,
|
|
200
221
|
deriveSiteHealthState,
|
|
201
222
|
describeError,
|
|
@@ -209,6 +230,19 @@ import {
|
|
|
209
230
|
forbidden,
|
|
210
231
|
formatIsoDateInTimeZone,
|
|
211
232
|
frameAncestorsHeaderValue,
|
|
233
|
+
googleAdsAccessibleCustomerDtoSchema,
|
|
234
|
+
googleAdsAccessibleCustomersResponseSchema,
|
|
235
|
+
googleAdsCampaignMetricsQuerySchema,
|
|
236
|
+
googleAdsRawSnapshotDtoSchema,
|
|
237
|
+
gtmAccountsResponseSchema,
|
|
238
|
+
gtmContainerListResponseSchema,
|
|
239
|
+
gtmGoogleAdsConversionIdMappingDtoSchema,
|
|
240
|
+
gtmGoogleAdsConversionLabelMappingDtoSchema,
|
|
241
|
+
gtmGoogleAdsConversionValueMappingDtoSchema,
|
|
242
|
+
gtmGoogleAdsCurrencyMappingDtoSchema,
|
|
243
|
+
gtmGoogleAdsTransactionIdMappingDtoSchema,
|
|
244
|
+
gtmRawSnapshotDtoSchema,
|
|
245
|
+
gtmWorkspaceListResponseSchema,
|
|
212
246
|
hostMatchesAnyDomain,
|
|
213
247
|
hostMatchesDomain,
|
|
214
248
|
hostOf,
|
|
@@ -233,6 +267,7 @@ import {
|
|
|
233
267
|
providerSupportsLocationContext,
|
|
234
268
|
registrableDomain,
|
|
235
269
|
resolveSnapshotRequestQueries,
|
|
270
|
+
retryAfterDelayMs,
|
|
236
271
|
serializeForInlineScript,
|
|
237
272
|
serializeRunError,
|
|
238
273
|
shiftIsoCalendarDate,
|
|
@@ -247,7 +282,7 @@ import {
|
|
|
247
282
|
validationError,
|
|
248
283
|
winnabilityClassLabel,
|
|
249
284
|
withRetry
|
|
250
|
-
} from "./chunk-
|
|
285
|
+
} from "./chunk-HJ7W2LXI.js";
|
|
251
286
|
|
|
252
287
|
// src/telemetry.ts
|
|
253
288
|
import crypto from "crypto";
|
|
@@ -576,13 +611,2720 @@ function checkLatestVersionForServer(opts) {
|
|
|
576
611
|
return buildUpdateAvailable(PKG_VERSION, memoryCache.latest);
|
|
577
612
|
}
|
|
578
613
|
|
|
614
|
+
// src/google-marketing-runtime.ts
|
|
615
|
+
import crypto3 from "crypto";
|
|
616
|
+
|
|
617
|
+
// ../integration-google-ads/src/constants.ts
|
|
618
|
+
var GOOGLE_ADS_API_VERSION = "v25";
|
|
619
|
+
var GOOGLE_ADS_API_HOST = "https://googleads.googleapis.com";
|
|
620
|
+
var GOOGLE_ADS_API_BASE = `${GOOGLE_ADS_API_HOST}/${GOOGLE_ADS_API_VERSION}`;
|
|
621
|
+
var GOOGLE_ADS_OAUTH_SCOPE = "https://www.googleapis.com/auth/adwords";
|
|
622
|
+
var GOOGLE_ADS_REQUEST_TIMEOUT_MS = 3e4;
|
|
623
|
+
var GOOGLE_ADS_MAX_RETRIES = 3;
|
|
624
|
+
var GOOGLE_ADS_RETRY_BASE_DELAY_MS = 1e3;
|
|
625
|
+
var GOOGLE_ADS_RETRY_MAX_DELAY_MS = 3e4;
|
|
626
|
+
var GOOGLE_ADS_MAX_RESULT_ROWS = 1e4;
|
|
627
|
+
var GOOGLE_ADS_MAX_DAILY_METRICS_DAYS = 90;
|
|
628
|
+
var GOOGLE_ADS_MAX_CAMPAIGN_FILTER_IDS = 200;
|
|
629
|
+
var GOOGLE_ADS_DEFAULT_ACCESSIBLE_CUSTOMER_DETAILS = 20;
|
|
630
|
+
var GOOGLE_ADS_MAX_ACCESSIBLE_CUSTOMER_DETAILS = 100;
|
|
631
|
+
var GOOGLE_ADS_CUSTOMER_DETAIL_CONCURRENCY = 5;
|
|
632
|
+
var GOOGLE_ADS_DEFAULT_CUSTOMER_CLIENTS = 100;
|
|
633
|
+
var GOOGLE_ADS_MAX_CUSTOMER_CLIENTS = 1e3;
|
|
634
|
+
var GOOGLE_ADS_DEFAULT_CUSTOMER_CLIENT_DEPTH = 1;
|
|
635
|
+
var GOOGLE_ADS_MAX_CUSTOMER_CLIENT_DEPTH = 10;
|
|
636
|
+
|
|
637
|
+
// ../integration-google-ads/src/goal-semantics.ts
|
|
638
|
+
function addInclusion(included, action, inclusion) {
|
|
639
|
+
const existing = included.get(action.resourceName);
|
|
640
|
+
if (existing) {
|
|
641
|
+
existing.includedBy.push(inclusion);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
included.set(action.resourceName, { conversionAction: action, includedBy: [inclusion] });
|
|
645
|
+
}
|
|
646
|
+
function resolveEffectiveCampaignGoalInputs(inputs) {
|
|
647
|
+
const actions = inputs.conversionActions.map((row) => row.conversionAction);
|
|
648
|
+
const actionsByResource = new Map(actions.map((action) => [action.resourceName, action]));
|
|
649
|
+
const customGoalsByResource = new Map(
|
|
650
|
+
inputs.customGoals.map((row) => [row.customConversionGoal.resourceName, row.customConversionGoal])
|
|
651
|
+
);
|
|
652
|
+
return inputs.campaignConfigs.map((row) => {
|
|
653
|
+
const config = row.conversionGoalCampaignConfig;
|
|
654
|
+
let categoryGoalSource = "UNKNOWN";
|
|
655
|
+
let categoryOriginGoals = [];
|
|
656
|
+
if (config.goalConfigLevel === "CUSTOMER") {
|
|
657
|
+
categoryGoalSource = "CUSTOMER";
|
|
658
|
+
categoryOriginGoals = inputs.customerGoals.map(({ customerConversionGoal: goal }) => ({
|
|
659
|
+
resourceName: goal.resourceName,
|
|
660
|
+
source: "CUSTOMER",
|
|
661
|
+
category: goal.category,
|
|
662
|
+
origin: goal.origin,
|
|
663
|
+
biddable: goal.biddable === true
|
|
664
|
+
}));
|
|
665
|
+
} else if (config.goalConfigLevel === "CAMPAIGN") {
|
|
666
|
+
categoryGoalSource = "CAMPAIGN";
|
|
667
|
+
categoryOriginGoals = inputs.campaignGoals.filter(({ campaignConversionGoal: goal }) => goal.campaign === config.campaign).map(({ campaignConversionGoal: goal }) => ({
|
|
668
|
+
resourceName: goal.resourceName,
|
|
669
|
+
source: "CAMPAIGN",
|
|
670
|
+
category: goal.category,
|
|
671
|
+
origin: goal.origin,
|
|
672
|
+
biddable: goal.biddable === true
|
|
673
|
+
}));
|
|
674
|
+
}
|
|
675
|
+
const included = /* @__PURE__ */ new Map();
|
|
676
|
+
for (const goal of categoryOriginGoals) {
|
|
677
|
+
if (!goal.biddable) continue;
|
|
678
|
+
for (const action of actions) {
|
|
679
|
+
if (action.primaryForGoal !== false && action.category === goal.category && action.origin === goal.origin) {
|
|
680
|
+
addInclusion(included, action, {
|
|
681
|
+
source: goal.source === "CUSTOMER" ? "CUSTOMER_GOAL" : "CAMPAIGN_GOAL",
|
|
682
|
+
goalResourceName: goal.resourceName,
|
|
683
|
+
category: goal.category,
|
|
684
|
+
origin: goal.origin
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
let customGoal = null;
|
|
690
|
+
let missingCustomGoalResourceName;
|
|
691
|
+
const missingConversionActionResourceNames = [];
|
|
692
|
+
if (config.goalConfigLevel === "CAMPAIGN" && config.customConversionGoal) {
|
|
693
|
+
customGoal = customGoalsByResource.get(config.customConversionGoal) ?? null;
|
|
694
|
+
if (!customGoal) {
|
|
695
|
+
missingCustomGoalResourceName = config.customConversionGoal;
|
|
696
|
+
} else {
|
|
697
|
+
for (const resourceName of customGoal.conversionActions ?? []) {
|
|
698
|
+
const action = actionsByResource.get(resourceName);
|
|
699
|
+
if (!action) {
|
|
700
|
+
missingConversionActionResourceNames.push(resourceName);
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
addInclusion(included, action, {
|
|
704
|
+
source: "CUSTOM_GOAL",
|
|
705
|
+
goalResourceName: customGoal.resourceName
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return {
|
|
711
|
+
campaign: row.campaign,
|
|
712
|
+
config,
|
|
713
|
+
categoryGoalSource,
|
|
714
|
+
categoryOriginGoals,
|
|
715
|
+
customGoal,
|
|
716
|
+
conversionActions: [...included.values()],
|
|
717
|
+
...missingCustomGoalResourceName ? { missingCustomGoalResourceName } : {},
|
|
718
|
+
missingConversionActionResourceNames
|
|
719
|
+
};
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// ../integration-google-ads/src/types.ts
|
|
724
|
+
var GoogleAdsApiError = class extends Error {
|
|
725
|
+
constructor(message, status, providerStatus, requestId, retryAfter) {
|
|
726
|
+
super(message);
|
|
727
|
+
this.status = status;
|
|
728
|
+
this.providerStatus = providerStatus;
|
|
729
|
+
this.requestId = requestId;
|
|
730
|
+
this.retryAfter = retryAfter;
|
|
731
|
+
}
|
|
732
|
+
status;
|
|
733
|
+
providerStatus;
|
|
734
|
+
requestId;
|
|
735
|
+
retryAfter;
|
|
736
|
+
name = "GoogleAdsApiError";
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
// ../integration-google-ads/src/queries.ts
|
|
740
|
+
var DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
741
|
+
function selectQuery(fields, resource, clauses) {
|
|
742
|
+
return [
|
|
743
|
+
"SELECT",
|
|
744
|
+
...fields.map((field, index) => ` ${field}${index === fields.length - 1 ? "" : ","}`),
|
|
745
|
+
`FROM ${resource}`,
|
|
746
|
+
...clauses
|
|
747
|
+
].join("\n");
|
|
748
|
+
}
|
|
749
|
+
function validatedLimit(limit) {
|
|
750
|
+
const value = limit ?? GOOGLE_ADS_MAX_RESULT_ROWS;
|
|
751
|
+
if (!Number.isInteger(value) || value < 1 || value > GOOGLE_ADS_MAX_RESULT_ROWS) {
|
|
752
|
+
throw new GoogleAdsApiError(
|
|
753
|
+
`Limit must be an integer between 1 and ${GOOGLE_ADS_MAX_RESULT_ROWS}`,
|
|
754
|
+
400
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
return value;
|
|
758
|
+
}
|
|
759
|
+
function normalizeGoogleAdsCustomerId(value, label = "Customer ID") {
|
|
760
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
761
|
+
throw new GoogleAdsApiError(`${label} is required`, 400);
|
|
762
|
+
}
|
|
763
|
+
const normalized = value.trim().replaceAll("-", "");
|
|
764
|
+
if (!/^\d{10}$/.test(normalized)) {
|
|
765
|
+
throw new GoogleAdsApiError(`${label} must contain exactly 10 digits`, 400);
|
|
766
|
+
}
|
|
767
|
+
return normalized;
|
|
768
|
+
}
|
|
769
|
+
function normalizeGoogleAdsCustomerClientsOptions(options = {}) {
|
|
770
|
+
const maxDepth = options.maxDepth ?? GOOGLE_ADS_DEFAULT_CUSTOMER_CLIENT_DEPTH;
|
|
771
|
+
if (!Number.isInteger(maxDepth) || maxDepth < 0 || maxDepth > GOOGLE_ADS_MAX_CUSTOMER_CLIENT_DEPTH) {
|
|
772
|
+
throw new GoogleAdsApiError(
|
|
773
|
+
`Customer-client depth must be an integer between 0 and ${GOOGLE_ADS_MAX_CUSTOMER_CLIENT_DEPTH}`,
|
|
774
|
+
400
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
const maxCustomers = options.maxCustomers ?? GOOGLE_ADS_DEFAULT_CUSTOMER_CLIENTS;
|
|
778
|
+
if (!Number.isInteger(maxCustomers) || maxCustomers < 1 || maxCustomers > GOOGLE_ADS_MAX_CUSTOMER_CLIENTS) {
|
|
779
|
+
throw new GoogleAdsApiError(
|
|
780
|
+
`Customer-client limit must be an integer between 1 and ${GOOGLE_ADS_MAX_CUSTOMER_CLIENTS}`,
|
|
781
|
+
400
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
return { maxDepth, maxCustomers };
|
|
785
|
+
}
|
|
786
|
+
function buildCustomerDetailsQuery() {
|
|
787
|
+
return selectQuery([
|
|
788
|
+
"customer.resource_name",
|
|
789
|
+
"customer.id",
|
|
790
|
+
"customer.descriptive_name",
|
|
791
|
+
"customer.currency_code",
|
|
792
|
+
"customer.time_zone",
|
|
793
|
+
"customer.manager",
|
|
794
|
+
"customer.test_account",
|
|
795
|
+
"customer.status",
|
|
796
|
+
"customer.auto_tagging_enabled",
|
|
797
|
+
"customer.conversion_tracking_setting.conversion_tracking_id",
|
|
798
|
+
"customer.conversion_tracking_setting.conversion_tracking_status",
|
|
799
|
+
"customer.conversion_tracking_setting.cross_account_conversion_tracking_id",
|
|
800
|
+
"customer.conversion_tracking_setting.google_ads_conversion_customer"
|
|
801
|
+
], "customer", ["LIMIT 1"]);
|
|
802
|
+
}
|
|
803
|
+
function buildCustomerClientsQuery(options = {}) {
|
|
804
|
+
const { maxDepth, maxCustomers } = normalizeGoogleAdsCustomerClientsOptions(options);
|
|
805
|
+
return selectQuery([
|
|
806
|
+
"customer_client.resource_name",
|
|
807
|
+
"customer_client.client_customer",
|
|
808
|
+
"customer_client.id",
|
|
809
|
+
"customer_client.descriptive_name",
|
|
810
|
+
"customer_client.currency_code",
|
|
811
|
+
"customer_client.time_zone",
|
|
812
|
+
"customer_client.manager",
|
|
813
|
+
"customer_client.test_account",
|
|
814
|
+
"customer_client.hidden",
|
|
815
|
+
"customer_client.status",
|
|
816
|
+
"customer_client.level"
|
|
817
|
+
], "customer_client", [
|
|
818
|
+
`WHERE customer_client.level <= ${maxDepth}`,
|
|
819
|
+
"ORDER BY customer_client.level ASC, customer_client.id ASC",
|
|
820
|
+
`LIMIT ${maxCustomers + 1}`
|
|
821
|
+
]);
|
|
822
|
+
}
|
|
823
|
+
function parseIsoDate(value, label) {
|
|
824
|
+
const match = DATE_PATTERN.exec(value);
|
|
825
|
+
if (!match) {
|
|
826
|
+
throw new GoogleAdsApiError(`${label} must use YYYY-MM-DD format`, 400);
|
|
827
|
+
}
|
|
828
|
+
const year = Number(match[1]);
|
|
829
|
+
const month = Number(match[2]);
|
|
830
|
+
const day = Number(match[3]);
|
|
831
|
+
const timestamp = Date.UTC(year, month - 1, day);
|
|
832
|
+
const parsed = new Date(timestamp);
|
|
833
|
+
if (parsed.getUTCFullYear() !== year || parsed.getUTCMonth() !== month - 1 || parsed.getUTCDate() !== day) {
|
|
834
|
+
throw new GoogleAdsApiError(`${label} must be a real calendar date`, 400);
|
|
835
|
+
}
|
|
836
|
+
return timestamp;
|
|
837
|
+
}
|
|
838
|
+
function dailyMetricClauses(options) {
|
|
839
|
+
const start = parseIsoDate(options.startDate, "Start date");
|
|
840
|
+
const end = parseIsoDate(options.endDate, "End date");
|
|
841
|
+
if (start > end) {
|
|
842
|
+
throw new GoogleAdsApiError("Start date must be on or before end date", 400);
|
|
843
|
+
}
|
|
844
|
+
const inclusiveDays = Math.floor((end - start) / 864e5) + 1;
|
|
845
|
+
if (inclusiveDays > GOOGLE_ADS_MAX_DAILY_METRICS_DAYS) {
|
|
846
|
+
throw new GoogleAdsApiError(
|
|
847
|
+
`Daily metrics range cannot exceed ${GOOGLE_ADS_MAX_DAILY_METRICS_DAYS} days`,
|
|
848
|
+
400
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
const filters = [
|
|
852
|
+
`segments.date BETWEEN '${options.startDate}' AND '${options.endDate}'`,
|
|
853
|
+
"campaign.status != 'REMOVED'"
|
|
854
|
+
];
|
|
855
|
+
if (options.campaignIds !== void 0) {
|
|
856
|
+
if (!Array.isArray(options.campaignIds) || options.campaignIds.length === 0) {
|
|
857
|
+
throw new GoogleAdsApiError("Campaign IDs must be a non-empty array when provided", 400);
|
|
858
|
+
}
|
|
859
|
+
if (options.campaignIds.length > GOOGLE_ADS_MAX_CAMPAIGN_FILTER_IDS) {
|
|
860
|
+
throw new GoogleAdsApiError(
|
|
861
|
+
`Campaign ID filter cannot exceed ${GOOGLE_ADS_MAX_CAMPAIGN_FILTER_IDS} IDs`,
|
|
862
|
+
400
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
const normalized = options.campaignIds.map((id) => {
|
|
866
|
+
const value = id.trim();
|
|
867
|
+
if (!/^\d+$/.test(value)) {
|
|
868
|
+
throw new GoogleAdsApiError("Campaign IDs must contain digits only", 400);
|
|
869
|
+
}
|
|
870
|
+
return value;
|
|
871
|
+
});
|
|
872
|
+
const unique = [...new Set(normalized)];
|
|
873
|
+
filters.push(`campaign.id IN (${unique.join(", ")})`);
|
|
874
|
+
}
|
|
875
|
+
return [
|
|
876
|
+
`WHERE ${filters.join("\n AND ")}`,
|
|
877
|
+
"ORDER BY segments.date ASC, campaign.id ASC",
|
|
878
|
+
`LIMIT ${validatedLimit(options.limit)}`
|
|
879
|
+
];
|
|
880
|
+
}
|
|
881
|
+
function buildCampaignsQuery(options = {}) {
|
|
882
|
+
const clauses = [];
|
|
883
|
+
if (!options.includeRemoved) clauses.push("WHERE campaign.status != 'REMOVED'");
|
|
884
|
+
clauses.push("ORDER BY campaign.id ASC", `LIMIT ${validatedLimit(options.limit)}`);
|
|
885
|
+
return selectQuery([
|
|
886
|
+
"campaign.resource_name",
|
|
887
|
+
"campaign.id",
|
|
888
|
+
"campaign.name",
|
|
889
|
+
"campaign.status",
|
|
890
|
+
"campaign.primary_status",
|
|
891
|
+
"campaign.serving_status",
|
|
892
|
+
"campaign.advertising_channel_type",
|
|
893
|
+
"campaign.advertising_channel_sub_type",
|
|
894
|
+
"campaign.campaign_budget",
|
|
895
|
+
"campaign.bidding_strategy",
|
|
896
|
+
"campaign.bidding_strategy_type",
|
|
897
|
+
"campaign.maximize_conversions.target_cpa_micros",
|
|
898
|
+
"campaign.maximize_conversion_value.target_roas",
|
|
899
|
+
"campaign.target_cpa.target_cpa_micros",
|
|
900
|
+
"campaign.target_roas.target_roas",
|
|
901
|
+
"campaign.target_impression_share.cpc_bid_ceiling_micros",
|
|
902
|
+
"campaign.target_impression_share.location",
|
|
903
|
+
"campaign.target_impression_share.location_fraction_micros",
|
|
904
|
+
"campaign.target_spend.cpc_bid_ceiling_micros",
|
|
905
|
+
"campaign.target_spend.target_spend_micros",
|
|
906
|
+
"campaign_budget.resource_name",
|
|
907
|
+
"campaign_budget.id",
|
|
908
|
+
"campaign_budget.name",
|
|
909
|
+
"campaign_budget.amount_micros",
|
|
910
|
+
"campaign_budget.total_amount_micros",
|
|
911
|
+
"campaign_budget.status",
|
|
912
|
+
"campaign_budget.explicitly_shared",
|
|
913
|
+
"bidding_strategy.resource_name",
|
|
914
|
+
"bidding_strategy.id",
|
|
915
|
+
"bidding_strategy.name",
|
|
916
|
+
"bidding_strategy.status",
|
|
917
|
+
"bidding_strategy.type",
|
|
918
|
+
"bidding_strategy.maximize_conversions.target_cpa_micros",
|
|
919
|
+
"bidding_strategy.maximize_conversion_value.target_roas",
|
|
920
|
+
"bidding_strategy.target_cpa.target_cpa_micros",
|
|
921
|
+
"bidding_strategy.target_roas.target_roas"
|
|
922
|
+
], "campaign", clauses);
|
|
923
|
+
}
|
|
924
|
+
function buildConversionActionsQuery(options = {}) {
|
|
925
|
+
const clauses = [];
|
|
926
|
+
if (!options.includeRemoved) clauses.push("WHERE conversion_action.status != 'REMOVED'");
|
|
927
|
+
clauses.push("ORDER BY conversion_action.id ASC", `LIMIT ${validatedLimit(options.limit)}`);
|
|
928
|
+
return selectQuery([
|
|
929
|
+
"conversion_action.resource_name",
|
|
930
|
+
"conversion_action.id",
|
|
931
|
+
"conversion_action.name",
|
|
932
|
+
"conversion_action.status",
|
|
933
|
+
"conversion_action.type",
|
|
934
|
+
"conversion_action.category",
|
|
935
|
+
"conversion_action.origin",
|
|
936
|
+
"conversion_action.owner_customer",
|
|
937
|
+
"conversion_action.primary_for_goal",
|
|
938
|
+
"conversion_action.include_in_conversions_metric",
|
|
939
|
+
"conversion_action.counting_type",
|
|
940
|
+
"conversion_action.click_through_lookback_window_days",
|
|
941
|
+
"conversion_action.view_through_lookback_window_days",
|
|
942
|
+
"conversion_action.attribution_model_settings.attribution_model",
|
|
943
|
+
"conversion_action.value_settings.always_use_default_value",
|
|
944
|
+
"conversion_action.value_settings.default_currency_code",
|
|
945
|
+
"conversion_action.value_settings.default_value",
|
|
946
|
+
"conversion_action.google_analytics_4_settings.event_name",
|
|
947
|
+
"conversion_action.google_analytics_4_settings.property_id",
|
|
948
|
+
"conversion_action.google_analytics_4_settings.property_name",
|
|
949
|
+
"conversion_action.tag_snippets"
|
|
950
|
+
], "conversion_action", clauses);
|
|
951
|
+
}
|
|
952
|
+
function buildCustomerConversionGoalsQuery(limit) {
|
|
953
|
+
return selectQuery([
|
|
954
|
+
"customer_conversion_goal.resource_name",
|
|
955
|
+
"customer_conversion_goal.category",
|
|
956
|
+
"customer_conversion_goal.origin",
|
|
957
|
+
"customer_conversion_goal.biddable"
|
|
958
|
+
], "customer_conversion_goal", [
|
|
959
|
+
"ORDER BY customer_conversion_goal.category ASC, customer_conversion_goal.origin ASC",
|
|
960
|
+
`LIMIT ${validatedLimit(limit)}`
|
|
961
|
+
]);
|
|
962
|
+
}
|
|
963
|
+
function buildCampaignConversionGoalsQuery(limit) {
|
|
964
|
+
return selectQuery([
|
|
965
|
+
"campaign_conversion_goal.resource_name",
|
|
966
|
+
"campaign_conversion_goal.campaign",
|
|
967
|
+
"campaign_conversion_goal.category",
|
|
968
|
+
"campaign_conversion_goal.origin",
|
|
969
|
+
"campaign_conversion_goal.biddable",
|
|
970
|
+
"campaign.resource_name",
|
|
971
|
+
"campaign.id",
|
|
972
|
+
"campaign.name",
|
|
973
|
+
"campaign.status"
|
|
974
|
+
], "campaign_conversion_goal", [
|
|
975
|
+
"WHERE campaign.status != 'REMOVED'",
|
|
976
|
+
"ORDER BY campaign.id ASC, campaign_conversion_goal.category ASC, campaign_conversion_goal.origin ASC",
|
|
977
|
+
`LIMIT ${validatedLimit(limit)}`
|
|
978
|
+
]);
|
|
979
|
+
}
|
|
980
|
+
function buildCustomConversionGoalsQuery(limit) {
|
|
981
|
+
return selectQuery([
|
|
982
|
+
"custom_conversion_goal.resource_name",
|
|
983
|
+
"custom_conversion_goal.id",
|
|
984
|
+
"custom_conversion_goal.name",
|
|
985
|
+
"custom_conversion_goal.status",
|
|
986
|
+
"custom_conversion_goal.conversion_actions"
|
|
987
|
+
], "custom_conversion_goal", [
|
|
988
|
+
"ORDER BY custom_conversion_goal.id ASC",
|
|
989
|
+
`LIMIT ${validatedLimit(limit)}`
|
|
990
|
+
]);
|
|
991
|
+
}
|
|
992
|
+
function buildConversionGoalCampaignConfigsQuery(limit) {
|
|
993
|
+
return selectQuery([
|
|
994
|
+
"conversion_goal_campaign_config.resource_name",
|
|
995
|
+
"conversion_goal_campaign_config.campaign",
|
|
996
|
+
"conversion_goal_campaign_config.custom_conversion_goal",
|
|
997
|
+
"conversion_goal_campaign_config.goal_config_level",
|
|
998
|
+
"campaign.resource_name",
|
|
999
|
+
"campaign.id",
|
|
1000
|
+
"campaign.name",
|
|
1001
|
+
"campaign.status"
|
|
1002
|
+
], "conversion_goal_campaign_config", [
|
|
1003
|
+
"WHERE campaign.status != 'REMOVED'",
|
|
1004
|
+
"ORDER BY campaign.id ASC",
|
|
1005
|
+
`LIMIT ${validatedLimit(limit)}`
|
|
1006
|
+
]);
|
|
1007
|
+
}
|
|
1008
|
+
function buildDailyCampaignMetricsQuery(options) {
|
|
1009
|
+
return selectQuery([
|
|
1010
|
+
"segments.date",
|
|
1011
|
+
"campaign.resource_name",
|
|
1012
|
+
"campaign.id",
|
|
1013
|
+
"campaign.name",
|
|
1014
|
+
"campaign.status",
|
|
1015
|
+
"metrics.impressions",
|
|
1016
|
+
"metrics.clicks",
|
|
1017
|
+
"metrics.cost_micros",
|
|
1018
|
+
"metrics.average_cpc",
|
|
1019
|
+
"metrics.ctr",
|
|
1020
|
+
"metrics.conversions",
|
|
1021
|
+
"metrics.conversions_value",
|
|
1022
|
+
"metrics.all_conversions",
|
|
1023
|
+
"metrics.all_conversions_value",
|
|
1024
|
+
"metrics.search_impression_share"
|
|
1025
|
+
], "campaign", dailyMetricClauses(options));
|
|
1026
|
+
}
|
|
1027
|
+
function buildDailyConversionMetricsQuery(options) {
|
|
1028
|
+
return selectQuery([
|
|
1029
|
+
"segments.date",
|
|
1030
|
+
"segments.conversion_action",
|
|
1031
|
+
"segments.conversion_action_name",
|
|
1032
|
+
"segments.conversion_action_category",
|
|
1033
|
+
"campaign.resource_name",
|
|
1034
|
+
"campaign.id",
|
|
1035
|
+
"campaign.name",
|
|
1036
|
+
"campaign.status",
|
|
1037
|
+
"metrics.conversions",
|
|
1038
|
+
"metrics.conversions_value",
|
|
1039
|
+
"metrics.all_conversions",
|
|
1040
|
+
"metrics.all_conversions_value"
|
|
1041
|
+
], "campaign", dailyMetricClauses(options));
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// ../integration-google-ads/src/google-ads-client.ts
|
|
1045
|
+
function escapeRegExp(value) {
|
|
1046
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1047
|
+
}
|
|
1048
|
+
function sanitizeText(value, secrets) {
|
|
1049
|
+
let sanitized = value.replace(/(authorization\s*:\s*bearer\s+)[^\s,"'}]+/gi, "$1***").replace(/((?:developer|access)[_-]?token\s*[:=]\s*["']?)[^\s,"'}]+/gi, "$1***");
|
|
1050
|
+
for (const secret of secrets) {
|
|
1051
|
+
if (!secret) continue;
|
|
1052
|
+
sanitized = sanitized.replace(new RegExp(escapeRegExp(secret), "g"), "***");
|
|
1053
|
+
const encoded = encodeURIComponent(secret);
|
|
1054
|
+
if (encoded !== secret) {
|
|
1055
|
+
sanitized = sanitized.replace(new RegExp(escapeRegExp(encoded), "g"), "***");
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return sanitized.length <= 1e3 ? sanitized : `${sanitized.slice(0, 1e3)}... [truncated]`;
|
|
1059
|
+
}
|
|
1060
|
+
function safeRequestId(value) {
|
|
1061
|
+
if (typeof value !== "string") return void 0;
|
|
1062
|
+
const trimmed = value.trim();
|
|
1063
|
+
if (trimmed === "" || trimmed.length > 256 || /[\r\n]/.test(trimmed)) return void 0;
|
|
1064
|
+
return trimmed;
|
|
1065
|
+
}
|
|
1066
|
+
function findRequestId(value, depth = 0) {
|
|
1067
|
+
if (depth > 6 || value === null || typeof value !== "object") return void 0;
|
|
1068
|
+
if (Array.isArray(value)) {
|
|
1069
|
+
for (const item of value) {
|
|
1070
|
+
const found = findRequestId(item, depth + 1);
|
|
1071
|
+
if (found) return found;
|
|
1072
|
+
}
|
|
1073
|
+
return void 0;
|
|
1074
|
+
}
|
|
1075
|
+
const record = value;
|
|
1076
|
+
const direct = safeRequestId(record.requestId) ?? safeRequestId(record.request_id);
|
|
1077
|
+
if (direct) return direct;
|
|
1078
|
+
for (const nested of Object.values(record)) {
|
|
1079
|
+
const found = findRequestId(nested, depth + 1);
|
|
1080
|
+
if (found) return found;
|
|
1081
|
+
}
|
|
1082
|
+
return void 0;
|
|
1083
|
+
}
|
|
1084
|
+
function providerErrorParts(payload) {
|
|
1085
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) return {};
|
|
1086
|
+
const envelope = payload;
|
|
1087
|
+
const message = typeof envelope.error?.message === "string" ? envelope.error.message : void 0;
|
|
1088
|
+
const providerStatus = typeof envelope.error?.status === "string" ? envelope.error.status : void 0;
|
|
1089
|
+
const requestId = findRequestId(envelope.error?.details);
|
|
1090
|
+
return { message, providerStatus, requestId };
|
|
1091
|
+
}
|
|
1092
|
+
function validateCredential(value, label) {
|
|
1093
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
1094
|
+
throw new GoogleAdsApiError(`${label} is required`, 400);
|
|
1095
|
+
}
|
|
1096
|
+
return value.trim();
|
|
1097
|
+
}
|
|
1098
|
+
function validatedAccessibleCustomerLimit(value) {
|
|
1099
|
+
const limit = value ?? GOOGLE_ADS_DEFAULT_ACCESSIBLE_CUSTOMER_DETAILS;
|
|
1100
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > GOOGLE_ADS_MAX_ACCESSIBLE_CUSTOMER_DETAILS) {
|
|
1101
|
+
throw new GoogleAdsApiError(
|
|
1102
|
+
`Accessible customer detail limit must be an integer between 1 and ${GOOGLE_ADS_MAX_ACCESSIBLE_CUSTOMER_DETAILS}`,
|
|
1103
|
+
400
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
return limit;
|
|
1107
|
+
}
|
|
1108
|
+
function customerIdFromResourceName(resourceName) {
|
|
1109
|
+
const match = /^customers\/(\d{10})$/.exec(resourceName);
|
|
1110
|
+
if (!match) {
|
|
1111
|
+
throw new GoogleAdsApiError(
|
|
1112
|
+
"Google Ads API returned an invalid accessible customer resource name",
|
|
1113
|
+
502
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
return match[1];
|
|
1117
|
+
}
|
|
1118
|
+
function isCustomerClientRow(row) {
|
|
1119
|
+
return row.customerClient !== void 0;
|
|
1120
|
+
}
|
|
1121
|
+
var GoogleAdsClient = class {
|
|
1122
|
+
#accessToken;
|
|
1123
|
+
#developerToken;
|
|
1124
|
+
#loginCustomerId;
|
|
1125
|
+
#fetch;
|
|
1126
|
+
#requestTimeoutMs;
|
|
1127
|
+
#retry;
|
|
1128
|
+
constructor(credentials, options = {}) {
|
|
1129
|
+
this.#accessToken = validateCredential(credentials.accessToken, "Access token");
|
|
1130
|
+
this.#developerToken = validateCredential(credentials.developerToken, "Developer token");
|
|
1131
|
+
this.#loginCustomerId = credentials.loginCustomerId === void 0 ? void 0 : normalizeGoogleAdsCustomerId(credentials.loginCustomerId, "Login customer ID");
|
|
1132
|
+
this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
1133
|
+
this.#requestTimeoutMs = options.requestTimeoutMs ?? GOOGLE_ADS_REQUEST_TIMEOUT_MS;
|
|
1134
|
+
if (!Number.isInteger(this.#requestTimeoutMs) || this.#requestTimeoutMs < 1) {
|
|
1135
|
+
throw new GoogleAdsApiError("Request timeout must be a positive integer in milliseconds", 400);
|
|
1136
|
+
}
|
|
1137
|
+
this.#retry = options.retry ?? {};
|
|
1138
|
+
}
|
|
1139
|
+
async listAccessibleCustomers() {
|
|
1140
|
+
const result = await this.#requestJson(
|
|
1141
|
+
"list-accessible-customers",
|
|
1142
|
+
"/customers:listAccessibleCustomers",
|
|
1143
|
+
{ method: "GET" },
|
|
1144
|
+
false
|
|
1145
|
+
);
|
|
1146
|
+
const resourceNames = result.data.resourceNames ?? [];
|
|
1147
|
+
if (!Array.isArray(resourceNames) || resourceNames.some((name) => typeof name !== "string")) {
|
|
1148
|
+
throw new GoogleAdsApiError(
|
|
1149
|
+
"Google Ads API returned an invalid accessible-customer response",
|
|
1150
|
+
502,
|
|
1151
|
+
void 0,
|
|
1152
|
+
result.metadata.requestId
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
return { ...result, data: { resourceNames } };
|
|
1156
|
+
}
|
|
1157
|
+
async getCustomerDetails(customerId) {
|
|
1158
|
+
const result = await this.#searchStream(
|
|
1159
|
+
customerId,
|
|
1160
|
+
"customer-details",
|
|
1161
|
+
buildCustomerDetailsQuery()
|
|
1162
|
+
);
|
|
1163
|
+
if (result.data.length !== 1 || !result.data[0]?.customer) {
|
|
1164
|
+
throw new GoogleAdsApiError(
|
|
1165
|
+
"Google Ads API returned an invalid customer detail response",
|
|
1166
|
+
502,
|
|
1167
|
+
void 0,
|
|
1168
|
+
result.metadata.requestId
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
return { data: result.data[0].customer, metadata: result.metadata };
|
|
1172
|
+
}
|
|
1173
|
+
async listAccessibleCustomerDetails(options = {}) {
|
|
1174
|
+
const maxCustomers = validatedAccessibleCustomerLimit(options.maxCustomers);
|
|
1175
|
+
const accessible = await this.listAccessibleCustomers();
|
|
1176
|
+
const resourceNames = accessible.data.resourceNames.slice(0, maxCustomers);
|
|
1177
|
+
const attempts = await mapWithConcurrency(
|
|
1178
|
+
resourceNames,
|
|
1179
|
+
GOOGLE_ADS_CUSTOMER_DETAIL_CONCURRENCY,
|
|
1180
|
+
async (resourceName) => {
|
|
1181
|
+
let customerId;
|
|
1182
|
+
try {
|
|
1183
|
+
customerId = customerIdFromResourceName(resourceName);
|
|
1184
|
+
const result = await this.getCustomerDetails(customerId);
|
|
1185
|
+
return { customer: result.data, metadata: result.metadata };
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
const googleAdsError = error instanceof GoogleAdsApiError ? error : void 0;
|
|
1188
|
+
const detail = describeError(error);
|
|
1189
|
+
return {
|
|
1190
|
+
failure: {
|
|
1191
|
+
resourceName,
|
|
1192
|
+
...customerId ? { customerId } : {},
|
|
1193
|
+
...googleAdsError?.status !== void 0 ? { status: googleAdsError.status } : {},
|
|
1194
|
+
...googleAdsError?.providerStatus ? { providerStatus: googleAdsError.providerStatus } : {},
|
|
1195
|
+
...googleAdsError?.requestId ? { requestId: googleAdsError.requestId } : {},
|
|
1196
|
+
message: sanitizeText(detail, this.#secrets())
|
|
1197
|
+
},
|
|
1198
|
+
...customerId ? {
|
|
1199
|
+
metadata: {
|
|
1200
|
+
apiVersion: GOOGLE_ADS_API_VERSION,
|
|
1201
|
+
operation: "customer-details",
|
|
1202
|
+
...googleAdsError?.requestId ? { requestId: googleAdsError.requestId } : {}
|
|
1203
|
+
}
|
|
1204
|
+
} : {}
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
);
|
|
1209
|
+
const customers = attempts.flatMap((attempt) => attempt.customer ? [attempt.customer] : []);
|
|
1210
|
+
const failures = attempts.flatMap((attempt) => attempt.failure ? [attempt.failure] : []);
|
|
1211
|
+
const omitted = accessible.data.resourceNames.length - resourceNames.length;
|
|
1212
|
+
return {
|
|
1213
|
+
data: {
|
|
1214
|
+
customers,
|
|
1215
|
+
failures,
|
|
1216
|
+
totalAccessible: accessible.data.resourceNames.length,
|
|
1217
|
+
attempted: resourceNames.length,
|
|
1218
|
+
returned: customers.length,
|
|
1219
|
+
truncated: omitted > 0,
|
|
1220
|
+
omitted
|
|
1221
|
+
},
|
|
1222
|
+
metadata: {
|
|
1223
|
+
apiVersion: GOOGLE_ADS_API_VERSION,
|
|
1224
|
+
requests: [
|
|
1225
|
+
accessible.metadata,
|
|
1226
|
+
...attempts.flatMap((attempt) => attempt.metadata ? [attempt.metadata] : [])
|
|
1227
|
+
]
|
|
1228
|
+
}
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
async getCustomerClients(customerId, options = {}) {
|
|
1232
|
+
const normalizedOptions = normalizeGoogleAdsCustomerClientsOptions(options);
|
|
1233
|
+
const result = await this.#searchStream(
|
|
1234
|
+
customerId,
|
|
1235
|
+
"customer-clients",
|
|
1236
|
+
buildCustomerClientsQuery(normalizedOptions)
|
|
1237
|
+
);
|
|
1238
|
+
if (!result.data.every(isCustomerClientRow)) {
|
|
1239
|
+
throw new GoogleAdsApiError(
|
|
1240
|
+
"Google Ads API returned an invalid customer-client response",
|
|
1241
|
+
502,
|
|
1242
|
+
void 0,
|
|
1243
|
+
result.metadata.requestId
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
const truncated = result.data.length > normalizedOptions.maxCustomers;
|
|
1247
|
+
const customerClients = result.data.slice(0, normalizedOptions.maxCustomers).map((row) => row.customerClient);
|
|
1248
|
+
return {
|
|
1249
|
+
data: {
|
|
1250
|
+
customerClients,
|
|
1251
|
+
maxDepth: normalizedOptions.maxDepth,
|
|
1252
|
+
returned: customerClients.length,
|
|
1253
|
+
truncated
|
|
1254
|
+
},
|
|
1255
|
+
metadata: result.metadata
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
async getCampaigns(customerId, options = {}) {
|
|
1259
|
+
return this.#searchStream(customerId, "campaigns", buildCampaignsQuery(options));
|
|
1260
|
+
}
|
|
1261
|
+
async getConversionActions(customerId, options = {}) {
|
|
1262
|
+
return this.#searchStream(customerId, "conversion-actions", buildConversionActionsQuery(options));
|
|
1263
|
+
}
|
|
1264
|
+
async getConversionGoals(customerId) {
|
|
1265
|
+
const normalizedCustomerId = normalizeGoogleAdsCustomerId(customerId);
|
|
1266
|
+
const [conversionActions, customerGoals, campaignGoals, customGoals, campaignConfigs] = await Promise.all([
|
|
1267
|
+
this.#searchStream(
|
|
1268
|
+
normalizedCustomerId,
|
|
1269
|
+
"conversion-actions",
|
|
1270
|
+
buildConversionActionsQuery({ includeRemoved: true })
|
|
1271
|
+
),
|
|
1272
|
+
this.#searchStream(
|
|
1273
|
+
normalizedCustomerId,
|
|
1274
|
+
"customer-conversion-goals",
|
|
1275
|
+
buildCustomerConversionGoalsQuery()
|
|
1276
|
+
),
|
|
1277
|
+
this.#searchStream(
|
|
1278
|
+
normalizedCustomerId,
|
|
1279
|
+
"campaign-conversion-goals",
|
|
1280
|
+
buildCampaignConversionGoalsQuery()
|
|
1281
|
+
),
|
|
1282
|
+
this.#searchStream(
|
|
1283
|
+
normalizedCustomerId,
|
|
1284
|
+
"custom-conversion-goals",
|
|
1285
|
+
buildCustomConversionGoalsQuery()
|
|
1286
|
+
),
|
|
1287
|
+
this.#searchStream(
|
|
1288
|
+
normalizedCustomerId,
|
|
1289
|
+
"conversion-goal-campaign-configs",
|
|
1290
|
+
buildConversionGoalCampaignConfigsQuery()
|
|
1291
|
+
)
|
|
1292
|
+
]);
|
|
1293
|
+
const raw = {
|
|
1294
|
+
conversionActions: conversionActions.data,
|
|
1295
|
+
customerGoals: customerGoals.data,
|
|
1296
|
+
campaignGoals: campaignGoals.data,
|
|
1297
|
+
customGoals: customGoals.data,
|
|
1298
|
+
campaignConfigs: campaignConfigs.data
|
|
1299
|
+
};
|
|
1300
|
+
return {
|
|
1301
|
+
data: {
|
|
1302
|
+
...raw,
|
|
1303
|
+
// A result exactly at the cap may have more rows upstream. Treat it
|
|
1304
|
+
// as incomplete rather than silently deriving absent campaign goals.
|
|
1305
|
+
campaignGoalsComplete: campaignGoals.data.length < GOOGLE_ADS_MAX_RESULT_ROWS,
|
|
1306
|
+
effectiveCampaignGoalInputs: resolveEffectiveCampaignGoalInputs(raw)
|
|
1307
|
+
},
|
|
1308
|
+
metadata: {
|
|
1309
|
+
apiVersion: GOOGLE_ADS_API_VERSION,
|
|
1310
|
+
requests: [
|
|
1311
|
+
conversionActions.metadata,
|
|
1312
|
+
customerGoals.metadata,
|
|
1313
|
+
campaignGoals.metadata,
|
|
1314
|
+
customGoals.metadata,
|
|
1315
|
+
campaignConfigs.metadata
|
|
1316
|
+
]
|
|
1317
|
+
}
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
async getDailyCampaignMetrics(customerId, options) {
|
|
1321
|
+
return this.#searchStream(
|
|
1322
|
+
customerId,
|
|
1323
|
+
"daily-campaign-metrics",
|
|
1324
|
+
buildDailyCampaignMetricsQuery(options)
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
async getDailyConversionMetrics(customerId, options) {
|
|
1328
|
+
return this.#searchStream(
|
|
1329
|
+
customerId,
|
|
1330
|
+
"daily-conversion-metrics",
|
|
1331
|
+
buildDailyConversionMetricsQuery(options)
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
async #searchStream(customerId, operation, query) {
|
|
1335
|
+
const normalizedCustomerId = normalizeGoogleAdsCustomerId(customerId);
|
|
1336
|
+
const result = await this.#requestJson(
|
|
1337
|
+
operation,
|
|
1338
|
+
`/customers/${normalizedCustomerId}/googleAds:searchStream`,
|
|
1339
|
+
{
|
|
1340
|
+
method: "POST",
|
|
1341
|
+
body: JSON.stringify({ query })
|
|
1342
|
+
},
|
|
1343
|
+
true
|
|
1344
|
+
);
|
|
1345
|
+
if (!Array.isArray(result.data)) {
|
|
1346
|
+
throw new GoogleAdsApiError(
|
|
1347
|
+
"Google Ads API returned an invalid searchStream response",
|
|
1348
|
+
502,
|
|
1349
|
+
void 0,
|
|
1350
|
+
result.metadata.requestId
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
const rows = [];
|
|
1354
|
+
let requestId = result.metadata.requestId;
|
|
1355
|
+
for (const chunk of result.data) {
|
|
1356
|
+
if (Array.isArray(chunk.results)) rows.push(...chunk.results);
|
|
1357
|
+
requestId ??= safeRequestId(chunk.requestId);
|
|
1358
|
+
}
|
|
1359
|
+
return {
|
|
1360
|
+
data: rows,
|
|
1361
|
+
metadata: {
|
|
1362
|
+
...result.metadata,
|
|
1363
|
+
...requestId ? { requestId } : {}
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
async #requestJson(operation, path9, init, includeLoginCustomerId) {
|
|
1368
|
+
const maxRetries = this.#retry.maxRetries ?? GOOGLE_ADS_MAX_RETRIES;
|
|
1369
|
+
const baseDelayMs = this.#retry.baseDelayMs ?? GOOGLE_ADS_RETRY_BASE_DELAY_MS;
|
|
1370
|
+
const maxDelayMs = this.#retry.maxDelayMs ?? GOOGLE_ADS_RETRY_MAX_DELAY_MS;
|
|
1371
|
+
return withRetry(
|
|
1372
|
+
() => this.#requestJsonOnce(operation, path9, init, includeLoginCustomerId),
|
|
1373
|
+
{
|
|
1374
|
+
maxRetries,
|
|
1375
|
+
baseDelayMs,
|
|
1376
|
+
maxDelayMs,
|
|
1377
|
+
jitter: this.#retry.jitter,
|
|
1378
|
+
sleep: this.#retry.sleep,
|
|
1379
|
+
isRetryable: isRetryableHttpError,
|
|
1380
|
+
computeDelayMs: (_attempt, error, defaultMs) => retryAfterDelayMs(error) ?? defaultMs
|
|
1381
|
+
}
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
async #requestJsonOnce(operation, path9, init, includeLoginCustomerId) {
|
|
1385
|
+
const headers = {
|
|
1386
|
+
Authorization: `Bearer ${this.#accessToken}`,
|
|
1387
|
+
"Content-Type": "application/json",
|
|
1388
|
+
"developer-token": this.#developerToken
|
|
1389
|
+
};
|
|
1390
|
+
if (includeLoginCustomerId && this.#loginCustomerId) {
|
|
1391
|
+
headers["login-customer-id"] = this.#loginCustomerId;
|
|
1392
|
+
}
|
|
1393
|
+
let response;
|
|
1394
|
+
try {
|
|
1395
|
+
response = await this.#fetch(`${GOOGLE_ADS_API_BASE}${path9}`, {
|
|
1396
|
+
...init,
|
|
1397
|
+
headers,
|
|
1398
|
+
signal: AbortSignal.timeout(this.#requestTimeoutMs)
|
|
1399
|
+
});
|
|
1400
|
+
} catch (error) {
|
|
1401
|
+
const detail = describeError(error);
|
|
1402
|
+
throw new GoogleAdsApiError(
|
|
1403
|
+
`Google Ads API request failed: ${sanitizeText(detail, this.#secrets())}`
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
const headerRequestId = safeRequestId(response.headers.get("request-id")) ?? safeRequestId(response.headers.get("google-ads-request-id"));
|
|
1407
|
+
const retryAfter = response.headers.get("retry-after") ?? void 0;
|
|
1408
|
+
let body;
|
|
1409
|
+
try {
|
|
1410
|
+
body = await response.text();
|
|
1411
|
+
} catch (error) {
|
|
1412
|
+
const detail = describeError(error);
|
|
1413
|
+
throw new GoogleAdsApiError(
|
|
1414
|
+
`Google Ads API response failed: ${sanitizeText(detail, this.#secrets())}`,
|
|
1415
|
+
void 0,
|
|
1416
|
+
void 0,
|
|
1417
|
+
headerRequestId
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
let payload;
|
|
1421
|
+
try {
|
|
1422
|
+
payload = body.trim() === "" ? null : JSON.parse(body);
|
|
1423
|
+
} catch {
|
|
1424
|
+
throw new GoogleAdsApiError(
|
|
1425
|
+
"Google Ads API returned invalid JSON",
|
|
1426
|
+
502,
|
|
1427
|
+
void 0,
|
|
1428
|
+
headerRequestId,
|
|
1429
|
+
retryAfter
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
if (!response.ok) {
|
|
1433
|
+
const provider = providerErrorParts(payload);
|
|
1434
|
+
const requestId = headerRequestId ?? provider.requestId;
|
|
1435
|
+
const rawDetail = provider.message ?? (body.trim() || response.statusText);
|
|
1436
|
+
const detail = sanitizeText(rawDetail, this.#secrets());
|
|
1437
|
+
throw new GoogleAdsApiError(
|
|
1438
|
+
`Google Ads API error (${response.status}): ${detail}`,
|
|
1439
|
+
response.status,
|
|
1440
|
+
provider.providerStatus,
|
|
1441
|
+
requestId,
|
|
1442
|
+
retryAfter
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
if (payload === null) {
|
|
1446
|
+
throw new GoogleAdsApiError(
|
|
1447
|
+
"Google Ads API returned an empty response",
|
|
1448
|
+
502,
|
|
1449
|
+
void 0,
|
|
1450
|
+
headerRequestId
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
return {
|
|
1454
|
+
data: payload,
|
|
1455
|
+
metadata: {
|
|
1456
|
+
apiVersion: GOOGLE_ADS_API_VERSION,
|
|
1457
|
+
operation,
|
|
1458
|
+
...headerRequestId ? { requestId: headerRequestId } : {}
|
|
1459
|
+
}
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
#secrets() {
|
|
1463
|
+
return [this.#accessToken, this.#developerToken];
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
|
|
1467
|
+
// ../integration-google-tag-manager/src/constants.ts
|
|
1468
|
+
var GTM_API_BASE = "https://tagmanager.googleapis.com/tagmanager/v2";
|
|
1469
|
+
var GTM_READONLY_SCOPE = "https://www.googleapis.com/auth/tagmanager.readonly";
|
|
1470
|
+
var GTM_REQUEST_TIMEOUT_MS = 3e4;
|
|
1471
|
+
var GTM_MAX_PAGES = 200;
|
|
1472
|
+
var GTM_SNAPSHOT_SCHEMA_VERSION = "gtm-container-snapshot-v1";
|
|
1473
|
+
var GTM_COMPARISON_SCHEMA_VERSION = "gtm-container-comparison-v1";
|
|
1474
|
+
var GTM_GOOGLE_ADS_CONVERSION_RECOGNIZER_VERSION = "google-ads-conversion-v1";
|
|
1475
|
+
var GTM_GOOGLE_ADS_CONVERSION_ASSESSMENT_SCHEMA_VERSION = "gtm-google-ads-conversion-assessment-v1";
|
|
1476
|
+
|
|
1477
|
+
// ../integration-google-tag-manager/src/checksum.ts
|
|
1478
|
+
import crypto2 from "crypto";
|
|
1479
|
+
function stableStringify(value) {
|
|
1480
|
+
if (value === null) return "null";
|
|
1481
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
1482
|
+
if (typeof value === "number" || typeof value === "boolean") return JSON.stringify(value);
|
|
1483
|
+
if (typeof value !== "object") throw new TypeError("Value is not JSON-serializable");
|
|
1484
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
1485
|
+
const object = value;
|
|
1486
|
+
const members = Object.keys(object).filter((key) => object[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`);
|
|
1487
|
+
return `{${members.join(",")}}`;
|
|
1488
|
+
}
|
|
1489
|
+
function checksumJson(value) {
|
|
1490
|
+
return crypto2.createHash("sha256").update(stableStringify(value)).digest("hex");
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// ../integration-google-tag-manager/src/types.ts
|
|
1494
|
+
var GtmApiError = class extends Error {
|
|
1495
|
+
status;
|
|
1496
|
+
providerStatus;
|
|
1497
|
+
reason;
|
|
1498
|
+
retryAfter;
|
|
1499
|
+
requestId;
|
|
1500
|
+
constructor(message, status, options = {}) {
|
|
1501
|
+
super(message);
|
|
1502
|
+
this.name = "GtmApiError";
|
|
1503
|
+
this.status = status;
|
|
1504
|
+
this.providerStatus = options.providerStatus ?? null;
|
|
1505
|
+
this.reason = options.reason ?? null;
|
|
1506
|
+
this.retryAfter = options.retryAfter ?? null;
|
|
1507
|
+
this.requestId = options.requestId ?? null;
|
|
1508
|
+
}
|
|
1509
|
+
};
|
|
1510
|
+
|
|
1511
|
+
// ../integration-google-tag-manager/src/http.ts
|
|
1512
|
+
function safeMessage(body, status, accessToken) {
|
|
1513
|
+
const payload = body;
|
|
1514
|
+
const providerMessage = payload?.error?.message;
|
|
1515
|
+
const message = typeof providerMessage === "string" ? providerMessage : `Tag Manager request failed (HTTP ${status})`;
|
|
1516
|
+
const redacted = accessToken ? message.replaceAll(accessToken, "[redacted]") : message;
|
|
1517
|
+
return redacted.replace(/[\r\n\t]+/g, " ").slice(0, 500);
|
|
1518
|
+
}
|
|
1519
|
+
function providerReason(body) {
|
|
1520
|
+
const payload = body;
|
|
1521
|
+
for (const detail of payload?.error?.details ?? []) {
|
|
1522
|
+
if (typeof detail.reason === "string") return detail.reason;
|
|
1523
|
+
}
|
|
1524
|
+
return null;
|
|
1525
|
+
}
|
|
1526
|
+
async function fetchOnce(url, accessToken, options) {
|
|
1527
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
1528
|
+
const controller = new AbortController();
|
|
1529
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? GTM_REQUEST_TIMEOUT_MS);
|
|
1530
|
+
try {
|
|
1531
|
+
const response = await fetchImpl(url, {
|
|
1532
|
+
method: "GET",
|
|
1533
|
+
headers: {
|
|
1534
|
+
authorization: `Bearer ${accessToken}`,
|
|
1535
|
+
accept: "application/json"
|
|
1536
|
+
},
|
|
1537
|
+
signal: controller.signal
|
|
1538
|
+
});
|
|
1539
|
+
const text = await response.text();
|
|
1540
|
+
let body = void 0;
|
|
1541
|
+
let invalidJson = false;
|
|
1542
|
+
if (text) {
|
|
1543
|
+
try {
|
|
1544
|
+
body = JSON.parse(text);
|
|
1545
|
+
} catch {
|
|
1546
|
+
invalidJson = true;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
if (!response.ok) {
|
|
1550
|
+
const payload = body;
|
|
1551
|
+
throw new GtmApiError(safeMessage(body, response.status, accessToken), response.status, {
|
|
1552
|
+
providerStatus: payload?.error?.status ?? null,
|
|
1553
|
+
reason: providerReason(body),
|
|
1554
|
+
retryAfter: response.headers.get("retry-after"),
|
|
1555
|
+
requestId: response.headers.get("x-request-id")
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
if (invalidJson) {
|
|
1559
|
+
throw new GtmApiError("Tag Manager returned invalid JSON", 502, {
|
|
1560
|
+
reason: "INVALID_PROVIDER_RESPONSE",
|
|
1561
|
+
retryAfter: response.headers.get("retry-after"),
|
|
1562
|
+
requestId: response.headers.get("x-request-id")
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
return body;
|
|
1566
|
+
} finally {
|
|
1567
|
+
clearTimeout(timeout);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
async function gtmFetchGet(url, accessToken, options) {
|
|
1571
|
+
return withRetry(() => fetchOnce(url, accessToken, options), {
|
|
1572
|
+
maxRetries: options.retry?.maxRetries ?? 3,
|
|
1573
|
+
baseDelayMs: options.retry?.baseDelayMs ?? 1e3,
|
|
1574
|
+
jitter: true,
|
|
1575
|
+
isRetryable: isRetryableHttpError,
|
|
1576
|
+
computeDelayMs: (_attempt, error, defaultMs) => retryAfterDelayMs(error) ?? defaultMs,
|
|
1577
|
+
sleep: options.retry?.sleep
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
// ../integration-google-tag-manager/src/snapshot.ts
|
|
1582
|
+
var COMPARISON_METADATA_KEYS = /* @__PURE__ */ new Set([
|
|
1583
|
+
"accountId",
|
|
1584
|
+
"containerId",
|
|
1585
|
+
"workspaceId",
|
|
1586
|
+
"path",
|
|
1587
|
+
"fingerprint",
|
|
1588
|
+
"tagManagerUrl"
|
|
1589
|
+
]);
|
|
1590
|
+
function comparisonValue(raw) {
|
|
1591
|
+
return Object.fromEntries(
|
|
1592
|
+
Object.entries(raw).filter(([key]) => !COMPARISON_METADATA_KEYS.has(key))
|
|
1593
|
+
);
|
|
1594
|
+
}
|
|
1595
|
+
function resourceId(kind, raw) {
|
|
1596
|
+
const explicit = kind === "tag" ? raw.tagId : kind === "trigger" ? raw.triggerId : kind === "variable" ? raw.variableId : kind === "folder" ? raw.folderId : raw.type;
|
|
1597
|
+
if (explicit) return explicit;
|
|
1598
|
+
if (raw.path) {
|
|
1599
|
+
const finalSegment = raw.path.split("/").filter(Boolean).at(-1);
|
|
1600
|
+
if (finalSegment) return finalSegment;
|
|
1601
|
+
}
|
|
1602
|
+
if (raw.name) return `name:${raw.name}`;
|
|
1603
|
+
return `anonymous:${checksumJson(raw)}`;
|
|
1604
|
+
}
|
|
1605
|
+
function snapshotEntity(kind, raw) {
|
|
1606
|
+
return {
|
|
1607
|
+
kind,
|
|
1608
|
+
id: resourceId(kind, raw),
|
|
1609
|
+
name: raw.name ?? null,
|
|
1610
|
+
fingerprint: "fingerprint" in raw && typeof raw.fingerprint === "string" ? raw.fingerprint : null,
|
|
1611
|
+
checksum: checksumJson(raw),
|
|
1612
|
+
contentChecksum: checksumJson(comparisonValue(raw)),
|
|
1613
|
+
raw
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
function snapshotEntities(lists) {
|
|
1617
|
+
const entities = {
|
|
1618
|
+
tags: lists.tags.map((raw) => snapshotEntity("tag", raw)),
|
|
1619
|
+
triggers: lists.triggers.map((raw) => snapshotEntity("trigger", raw)),
|
|
1620
|
+
variables: lists.variables.map((raw) => snapshotEntity("variable", raw)),
|
|
1621
|
+
folders: lists.folders.map((raw) => snapshotEntity("folder", raw)),
|
|
1622
|
+
builtInVariables: lists.builtInVariables.map((raw) => snapshotEntity("builtInVariable", raw))
|
|
1623
|
+
};
|
|
1624
|
+
entities.tags.sort((left, right) => left.id.localeCompare(right.id));
|
|
1625
|
+
entities.triggers.sort((left, right) => left.id.localeCompare(right.id));
|
|
1626
|
+
entities.variables.sort((left, right) => left.id.localeCompare(right.id));
|
|
1627
|
+
entities.folders.sort((left, right) => left.id.localeCompare(right.id));
|
|
1628
|
+
entities.builtInVariables.sort((left, right) => left.id.localeCompare(right.id));
|
|
1629
|
+
assertUniqueIdentities(entities);
|
|
1630
|
+
return entities;
|
|
1631
|
+
}
|
|
1632
|
+
function allEntities(sets) {
|
|
1633
|
+
return [
|
|
1634
|
+
...sets.tags,
|
|
1635
|
+
...sets.triggers,
|
|
1636
|
+
...sets.variables,
|
|
1637
|
+
...sets.folders,
|
|
1638
|
+
...sets.builtInVariables
|
|
1639
|
+
];
|
|
1640
|
+
}
|
|
1641
|
+
function assertUniqueIdentities(sets) {
|
|
1642
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1643
|
+
for (const entity of allEntities(sets)) {
|
|
1644
|
+
const identity = `${entity.kind}:${entity.id}`;
|
|
1645
|
+
if (seen.has(identity)) {
|
|
1646
|
+
throw new GtmApiError(`Tag Manager returned duplicate ${entity.kind} identity`, 502, {
|
|
1647
|
+
reason: "DUPLICATE_ENTITY_IDENTITY"
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
seen.add(identity);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
function snapshotChecksum(accountId, containerId, entities) {
|
|
1654
|
+
return checksumJson({
|
|
1655
|
+
accountId,
|
|
1656
|
+
containerId,
|
|
1657
|
+
entities: allEntities(entities).map((entity) => ({
|
|
1658
|
+
kind: entity.kind,
|
|
1659
|
+
id: entity.id,
|
|
1660
|
+
contentChecksum: entity.contentChecksum
|
|
1661
|
+
}))
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
function buildLiveSnapshot(version) {
|
|
1665
|
+
const entities = snapshotEntities({
|
|
1666
|
+
tags: version.tag ?? [],
|
|
1667
|
+
triggers: version.trigger ?? [],
|
|
1668
|
+
variables: version.variable ?? [],
|
|
1669
|
+
folders: version.folder ?? [],
|
|
1670
|
+
builtInVariables: version.builtInVariable ?? []
|
|
1671
|
+
});
|
|
1672
|
+
const accountId = version.accountId ?? null;
|
|
1673
|
+
const containerId = version.containerId ?? null;
|
|
1674
|
+
return {
|
|
1675
|
+
schemaVersion: GTM_SNAPSHOT_SCHEMA_VERSION,
|
|
1676
|
+
source: "live",
|
|
1677
|
+
identity: {
|
|
1678
|
+
accountId,
|
|
1679
|
+
containerId,
|
|
1680
|
+
containerVersionId: version.containerVersionId ?? null,
|
|
1681
|
+
workspaceId: null,
|
|
1682
|
+
path: version.path ?? null,
|
|
1683
|
+
fingerprint: version.fingerprint ?? null
|
|
1684
|
+
},
|
|
1685
|
+
entities,
|
|
1686
|
+
checksum: snapshotChecksum(accountId, containerId, entities),
|
|
1687
|
+
raw: { containerVersion: version }
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
function buildWorkspaceSnapshot(workspace, status, lists) {
|
|
1691
|
+
const entities = snapshotEntities(lists);
|
|
1692
|
+
const accountId = workspace.accountId ?? null;
|
|
1693
|
+
const containerId = workspace.containerId ?? null;
|
|
1694
|
+
return {
|
|
1695
|
+
schemaVersion: GTM_SNAPSHOT_SCHEMA_VERSION,
|
|
1696
|
+
source: "workspace",
|
|
1697
|
+
identity: {
|
|
1698
|
+
accountId,
|
|
1699
|
+
containerId,
|
|
1700
|
+
containerVersionId: null,
|
|
1701
|
+
workspaceId: workspace.workspaceId ?? null,
|
|
1702
|
+
path: workspace.path ?? null,
|
|
1703
|
+
fingerprint: workspace.fingerprint ?? null
|
|
1704
|
+
},
|
|
1705
|
+
entities,
|
|
1706
|
+
checksum: snapshotChecksum(accountId, containerId, entities),
|
|
1707
|
+
raw: { workspace, workspaceStatus: status }
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
function fingerprintMatches(live, draft) {
|
|
1711
|
+
if (!live?.fingerprint || !draft?.fingerprint) return null;
|
|
1712
|
+
return live.fingerprint === draft.fingerprint;
|
|
1713
|
+
}
|
|
1714
|
+
function compareContainerSnapshots(live, draft) {
|
|
1715
|
+
if (live.source !== "live" || draft.source !== "workspace") {
|
|
1716
|
+
throw new GtmApiError("Comparison requires one live snapshot and one workspace snapshot", 400, {
|
|
1717
|
+
reason: "INVALID_SNAPSHOT_PAIR"
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
if (live.identity.accountId !== draft.identity.accountId || live.identity.containerId !== draft.identity.containerId) {
|
|
1721
|
+
throw new GtmApiError("Live and workspace snapshots belong to different containers", 400, {
|
|
1722
|
+
reason: "CONTAINER_IDENTITY_MISMATCH"
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
const liveByIdentity = new Map(allEntities(live.entities).map((entity) => [`${entity.kind}:${entity.id}`, entity]));
|
|
1726
|
+
const draftByIdentity = new Map(allEntities(draft.entities).map((entity) => [`${entity.kind}:${entity.id}`, entity]));
|
|
1727
|
+
const identities = [.../* @__PURE__ */ new Set([...liveByIdentity.keys(), ...draftByIdentity.keys()])].sort();
|
|
1728
|
+
const changes = identities.map((identity) => {
|
|
1729
|
+
const liveEntity = liveByIdentity.get(identity);
|
|
1730
|
+
const draftEntity = draftByIdentity.get(identity);
|
|
1731
|
+
const contentMatches = Boolean(
|
|
1732
|
+
liveEntity && draftEntity && liveEntity.contentChecksum === draftEntity.contentChecksum
|
|
1733
|
+
);
|
|
1734
|
+
const status = !liveEntity ? "added" : !draftEntity ? "removed" : contentMatches ? "unchanged" : "changed";
|
|
1735
|
+
const entity = draftEntity ?? liveEntity;
|
|
1736
|
+
if (!entity) throw new Error("Entity comparison invariant failed");
|
|
1737
|
+
return {
|
|
1738
|
+
kind: entity.kind,
|
|
1739
|
+
id: entity.id,
|
|
1740
|
+
name: entity.name,
|
|
1741
|
+
status,
|
|
1742
|
+
contentMatches,
|
|
1743
|
+
providerFingerprintMatches: fingerprintMatches(liveEntity, draftEntity),
|
|
1744
|
+
live: liveEntity ?? null,
|
|
1745
|
+
draft: draftEntity ?? null
|
|
1746
|
+
};
|
|
1747
|
+
});
|
|
1748
|
+
const workspaceStatus = draft.raw.workspaceStatus ?? {};
|
|
1749
|
+
const workspaceChanges = workspaceStatus.workspaceChange ?? [];
|
|
1750
|
+
const mergeConflicts = workspaceStatus.mergeConflict ?? [];
|
|
1751
|
+
const hasConflicts = mergeConflicts.length > 0;
|
|
1752
|
+
const hasUnpublishedChanges = hasConflicts || workspaceChanges.length > 0 || changes.some((change) => change.status !== "unchanged");
|
|
1753
|
+
return {
|
|
1754
|
+
schemaVersion: GTM_COMPARISON_SCHEMA_VERSION,
|
|
1755
|
+
state: hasConflicts ? "conflicted" : hasUnpublishedChanges ? "unpublished-changes" : "in-sync",
|
|
1756
|
+
hasUnpublishedChanges,
|
|
1757
|
+
hasConflicts,
|
|
1758
|
+
liveChecksum: live.checksum,
|
|
1759
|
+
draftChecksum: draft.checksum,
|
|
1760
|
+
changes,
|
|
1761
|
+
workspaceChanges,
|
|
1762
|
+
mergeConflicts
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
// ../integration-google-tag-manager/src/client.ts
|
|
1767
|
+
var GTM_ID_SEGMENT = String.raw`[\w-]+`;
|
|
1768
|
+
var PATH_PATTERNS = {
|
|
1769
|
+
account: new RegExp(`^accounts/${GTM_ID_SEGMENT}$`),
|
|
1770
|
+
container: new RegExp(`^accounts/${GTM_ID_SEGMENT}/containers/${GTM_ID_SEGMENT}$`),
|
|
1771
|
+
workspace: new RegExp(
|
|
1772
|
+
`^accounts/${GTM_ID_SEGMENT}/containers/${GTM_ID_SEGMENT}/workspaces/${GTM_ID_SEGMENT}$`
|
|
1773
|
+
)
|
|
1774
|
+
};
|
|
1775
|
+
function assertAccessToken(accessToken) {
|
|
1776
|
+
if (typeof accessToken !== "string" || accessToken.trim() === "") {
|
|
1777
|
+
throw new GtmApiError("OAuth access token is required", 400, { reason: "INVALID_ACCESS_TOKEN" });
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
function assertPath(path9, kind) {
|
|
1781
|
+
if (!PATH_PATTERNS[kind].test(path9)) {
|
|
1782
|
+
throw new GtmApiError(`Invalid Tag Manager ${kind} resource path`, 400, {
|
|
1783
|
+
reason: "INVALID_RESOURCE_PATH"
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
function apiUrl(path9, pageToken) {
|
|
1788
|
+
const url = new URL(`${GTM_API_BASE}/${path9}`);
|
|
1789
|
+
if (pageToken !== void 0) url.searchParams.set("pageToken", pageToken);
|
|
1790
|
+
return url.toString();
|
|
1791
|
+
}
|
|
1792
|
+
async function listCollection(accessToken, path9, responseKey, options) {
|
|
1793
|
+
const collected = [];
|
|
1794
|
+
const seenTokens = /* @__PURE__ */ new Set();
|
|
1795
|
+
let pageToken;
|
|
1796
|
+
for (let page = 0; page < GTM_MAX_PAGES; page++) {
|
|
1797
|
+
const response = await gtmFetchGet(apiUrl(path9, pageToken), accessToken, options);
|
|
1798
|
+
const values = response[responseKey];
|
|
1799
|
+
if (values !== void 0 && !Array.isArray(values)) {
|
|
1800
|
+
throw new GtmApiError(`Tag Manager returned an invalid ${responseKey} list`, 502, {
|
|
1801
|
+
reason: "INVALID_PROVIDER_RESPONSE"
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
collected.push(...values ?? []);
|
|
1805
|
+
const nextPageToken = response.nextPageToken;
|
|
1806
|
+
if (nextPageToken === void 0 || nextPageToken === "") return collected;
|
|
1807
|
+
if (typeof nextPageToken !== "string" || seenTokens.has(nextPageToken)) {
|
|
1808
|
+
throw new GtmApiError("Tag Manager returned an invalid pagination token", 502, {
|
|
1809
|
+
reason: "INVALID_PROVIDER_PAGINATION"
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
seenTokens.add(nextPageToken);
|
|
1813
|
+
pageToken = nextPageToken;
|
|
1814
|
+
}
|
|
1815
|
+
throw new GtmApiError("Tag Manager pagination exceeded the safety limit", 502, {
|
|
1816
|
+
reason: "PAGINATION_LIMIT_EXCEEDED"
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
function createGoogleTagManagerClient(accessToken, options = {}) {
|
|
1820
|
+
assertAccessToken(accessToken);
|
|
1821
|
+
const listAccounts = () => listCollection(accessToken, "accounts", "account", options);
|
|
1822
|
+
const listContainers = async (accountPath2) => {
|
|
1823
|
+
assertPath(accountPath2, "account");
|
|
1824
|
+
return listCollection(accessToken, `${accountPath2}/containers`, "container", options);
|
|
1825
|
+
};
|
|
1826
|
+
const listWorkspaces = async (containerPath2) => {
|
|
1827
|
+
assertPath(containerPath2, "container");
|
|
1828
|
+
return listCollection(accessToken, `${containerPath2}/workspaces`, "workspace", options);
|
|
1829
|
+
};
|
|
1830
|
+
const getLiveContainerVersion = async (containerPath2) => {
|
|
1831
|
+
assertPath(containerPath2, "container");
|
|
1832
|
+
return gtmFetchGet(
|
|
1833
|
+
`${GTM_API_BASE}/${containerPath2}/versions:live`,
|
|
1834
|
+
accessToken,
|
|
1835
|
+
options
|
|
1836
|
+
);
|
|
1837
|
+
};
|
|
1838
|
+
const getLiveSnapshot = async (containerPath2) => buildLiveSnapshot(await getLiveContainerVersion(containerPath2));
|
|
1839
|
+
const getWorkspaceSnapshot = async (workspacePath2) => {
|
|
1840
|
+
assertPath(workspacePath2, "workspace");
|
|
1841
|
+
const [workspace, status, tags, triggers, variables, folders, builtInVariables] = await Promise.all([
|
|
1842
|
+
gtmFetchGet(`${GTM_API_BASE}/${workspacePath2}`, accessToken, options),
|
|
1843
|
+
gtmFetchGet(`${GTM_API_BASE}/${workspacePath2}/status`, accessToken, options),
|
|
1844
|
+
listCollection(accessToken, `${workspacePath2}/tags`, "tag", options),
|
|
1845
|
+
listCollection(accessToken, `${workspacePath2}/triggers`, "trigger", options),
|
|
1846
|
+
listCollection(accessToken, `${workspacePath2}/variables`, "variable", options),
|
|
1847
|
+
listCollection(accessToken, `${workspacePath2}/folders`, "folder", options),
|
|
1848
|
+
listCollection(
|
|
1849
|
+
accessToken,
|
|
1850
|
+
`${workspacePath2}/built_in_variables`,
|
|
1851
|
+
"builtInVariable",
|
|
1852
|
+
options
|
|
1853
|
+
)
|
|
1854
|
+
]);
|
|
1855
|
+
return buildWorkspaceSnapshot(workspace, status, { tags, triggers, variables, folders, builtInVariables });
|
|
1856
|
+
};
|
|
1857
|
+
const compareLiveAndWorkspace = async (containerPath2, workspacePath2) => {
|
|
1858
|
+
const [live, workspace] = await Promise.all([
|
|
1859
|
+
getLiveSnapshot(containerPath2),
|
|
1860
|
+
getWorkspaceSnapshot(workspacePath2)
|
|
1861
|
+
]);
|
|
1862
|
+
return compareContainerSnapshots(live, workspace);
|
|
1863
|
+
};
|
|
1864
|
+
return {
|
|
1865
|
+
listAccounts,
|
|
1866
|
+
listContainers,
|
|
1867
|
+
listWorkspaces,
|
|
1868
|
+
getLiveContainerVersion,
|
|
1869
|
+
getLiveSnapshot,
|
|
1870
|
+
getWorkspaceSnapshot,
|
|
1871
|
+
compareLiveAndWorkspace
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
// ../integration-google-tag-manager/src/recognizer.ts
|
|
1876
|
+
var GOOGLE_ADS_CONVERSION_TAG_TYPE = "awct";
|
|
1877
|
+
var URL_TRIGGER_TYPES = /* @__PURE__ */ new Set([
|
|
1878
|
+
"pageview",
|
|
1879
|
+
"domReady",
|
|
1880
|
+
"windowLoaded",
|
|
1881
|
+
"init",
|
|
1882
|
+
"consentInit",
|
|
1883
|
+
"serverPageview",
|
|
1884
|
+
"historyChange"
|
|
1885
|
+
]);
|
|
1886
|
+
function unknownReason(tagType) {
|
|
1887
|
+
if (tagType === "html") return "custom-html";
|
|
1888
|
+
if (tagType === "cvt" || tagType?.startsWith("cvt_")) return "custom-template";
|
|
1889
|
+
return "unsupported-tag-type";
|
|
1890
|
+
}
|
|
1891
|
+
function parametersByKey(parameters) {
|
|
1892
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1893
|
+
for (const parameter of parameters ?? []) {
|
|
1894
|
+
if (!parameter.key) continue;
|
|
1895
|
+
const values = byKey.get(parameter.key) ?? [];
|
|
1896
|
+
values.push(parameter);
|
|
1897
|
+
byKey.set(parameter.key, values);
|
|
1898
|
+
}
|
|
1899
|
+
return byKey;
|
|
1900
|
+
}
|
|
1901
|
+
function scalarMapping(byKey, key) {
|
|
1902
|
+
const parameters = byKey.get(key);
|
|
1903
|
+
if (parameters?.length !== 1) return null;
|
|
1904
|
+
const parameter = parameters[0];
|
|
1905
|
+
if (typeof parameter.value !== "string" || parameter.list || parameter.map) return null;
|
|
1906
|
+
const trimmed = parameter.value.trim();
|
|
1907
|
+
const hasVariable = parameter.value.includes("{{") && parameter.value.includes("}}");
|
|
1908
|
+
const onlyVariable = trimmed.startsWith("{{") && trimmed.endsWith("}}");
|
|
1909
|
+
return {
|
|
1910
|
+
parameterKey: key,
|
|
1911
|
+
value: parameter.value,
|
|
1912
|
+
source: onlyVariable ? "variable" : hasVariable ? "mixed" : "literal"
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
function conditions(trigger) {
|
|
1916
|
+
return [
|
|
1917
|
+
...trigger.customEventFilter ?? [],
|
|
1918
|
+
...trigger.filter ?? [],
|
|
1919
|
+
...trigger.autoEventFilter ?? []
|
|
1920
|
+
];
|
|
1921
|
+
}
|
|
1922
|
+
function conditionParameter(condition, key) {
|
|
1923
|
+
const parameter = condition.parameter?.find((candidate) => candidate.key === key);
|
|
1924
|
+
return typeof parameter?.value === "string" ? parameter.value : null;
|
|
1925
|
+
}
|
|
1926
|
+
function normalizedVariableName(value) {
|
|
1927
|
+
const trimmed = value.trim();
|
|
1928
|
+
const unwrapped = trimmed.startsWith("{{") && trimmed.endsWith("}}") ? trimmed.slice(2, -2) : trimmed;
|
|
1929
|
+
return unwrapped.trim().toLowerCase();
|
|
1930
|
+
}
|
|
1931
|
+
function isEventVariable(value) {
|
|
1932
|
+
const normalized = normalizedVariableName(value);
|
|
1933
|
+
return normalized === "_event" || normalized === "event" || normalized === "event name";
|
|
1934
|
+
}
|
|
1935
|
+
function isHostnameVariable(value) {
|
|
1936
|
+
const normalized = normalizedVariableName(value);
|
|
1937
|
+
return normalized === "page hostname" || normalized === "_hostname";
|
|
1938
|
+
}
|
|
1939
|
+
function customEventNames(trigger) {
|
|
1940
|
+
const names = [];
|
|
1941
|
+
for (const condition of trigger.customEventFilter ?? []) {
|
|
1942
|
+
const left = conditionParameter(condition, "arg0");
|
|
1943
|
+
const right = conditionParameter(condition, "arg1");
|
|
1944
|
+
if (left && right && isEventVariable(left)) names.push(right);
|
|
1945
|
+
}
|
|
1946
|
+
return names;
|
|
1947
|
+
}
|
|
1948
|
+
function filterMatchesHostname(operator, filterValue, hostname) {
|
|
1949
|
+
const expected = hostname.toLowerCase();
|
|
1950
|
+
const value = filterValue.toLowerCase();
|
|
1951
|
+
if (operator === "equals") return expected === value;
|
|
1952
|
+
if (operator === "contains") return expected.includes(value);
|
|
1953
|
+
if (operator === "startsWith") return expected.startsWith(value);
|
|
1954
|
+
if (operator === "endsWith") return expected.endsWith(value);
|
|
1955
|
+
if (operator === "matchRegex" || operator === "urlMatches") {
|
|
1956
|
+
try {
|
|
1957
|
+
return new RegExp(filterValue, "i").test(hostname);
|
|
1958
|
+
} catch {
|
|
1959
|
+
return false;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
return false;
|
|
1963
|
+
}
|
|
1964
|
+
function hostnameFilters(trigger, expectedHostname) {
|
|
1965
|
+
const filters = [];
|
|
1966
|
+
for (const condition of conditions(trigger)) {
|
|
1967
|
+
const left = conditionParameter(condition, "arg0");
|
|
1968
|
+
const right = conditionParameter(condition, "arg1");
|
|
1969
|
+
if (!left || !right || !isHostnameVariable(left)) continue;
|
|
1970
|
+
const negated = conditionParameter(condition, "negate") === "true";
|
|
1971
|
+
filters.push({
|
|
1972
|
+
operator: condition.type ?? "conditionTypeUnspecified",
|
|
1973
|
+
value: right,
|
|
1974
|
+
negated,
|
|
1975
|
+
matchesExpectedHostname: expectedHostname === void 0 ? null : !negated && filterMatchesHostname(condition.type ?? "", right, expectedHostname)
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
return filters;
|
|
1979
|
+
}
|
|
1980
|
+
function triggerStrategy(triggers) {
|
|
1981
|
+
if (triggers.length === 0) return "none";
|
|
1982
|
+
const categories = new Set(triggers.map((trigger) => {
|
|
1983
|
+
if (trigger.type === "customEvent") return "custom-event";
|
|
1984
|
+
if (trigger.type && URL_TRIGGER_TYPES.has(trigger.type)) return "url-based";
|
|
1985
|
+
return "unknown";
|
|
1986
|
+
}));
|
|
1987
|
+
if (categories.size > 1) return "mixed";
|
|
1988
|
+
return [...categories][0];
|
|
1989
|
+
}
|
|
1990
|
+
function unknownAssessment(tag, reason, triggers, options) {
|
|
1991
|
+
const firingTriggerIds = [...tag.firingTriggerId ?? []];
|
|
1992
|
+
const triggerById = /* @__PURE__ */ new Map();
|
|
1993
|
+
for (const trigger of triggers) {
|
|
1994
|
+
if (typeof trigger.triggerId === "string") triggerById.set(trigger.triggerId, trigger);
|
|
1995
|
+
}
|
|
1996
|
+
const resolved = firingTriggerIds.map((id) => triggerById.get(id)).filter((trigger) => trigger !== void 0);
|
|
1997
|
+
return {
|
|
1998
|
+
schemaVersion: GTM_GOOGLE_ADS_CONVERSION_ASSESSMENT_SCHEMA_VERSION,
|
|
1999
|
+
recognizerVersion: GTM_GOOGLE_ADS_CONVERSION_RECOGNIZER_VERSION,
|
|
2000
|
+
recognition: { status: "unknown", reason },
|
|
2001
|
+
review: { status: "needs-review", reasons: ["tag-unknown"] },
|
|
2002
|
+
tag: {
|
|
2003
|
+
id: tag.tagId ?? null,
|
|
2004
|
+
name: tag.name ?? null,
|
|
2005
|
+
type: tag.type ?? null,
|
|
2006
|
+
checksum: checksumJson(tag)
|
|
2007
|
+
},
|
|
2008
|
+
conversion: null,
|
|
2009
|
+
trigger: {
|
|
2010
|
+
strategy: triggerStrategy(resolved),
|
|
2011
|
+
firingTriggerIds,
|
|
2012
|
+
resolvedTriggerIds: resolved.flatMap((trigger) => trigger.triggerId ? [trigger.triggerId] : []),
|
|
2013
|
+
unresolvedTriggerIds: firingTriggerIds.filter((id) => !triggerById.has(id)),
|
|
2014
|
+
customEventNames: [...new Set(resolved.flatMap(customEventNames))],
|
|
2015
|
+
hostnameFilters: resolved.flatMap((trigger) => hostnameFilters(trigger, options.expectedHostname))
|
|
2016
|
+
}
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
function recognizeGoogleAdsConversionTag(tag, triggers, options = {}) {
|
|
2020
|
+
if (tag.type !== GOOGLE_ADS_CONVERSION_TAG_TYPE) {
|
|
2021
|
+
return unknownAssessment(tag, unknownReason(tag.type), triggers, options);
|
|
2022
|
+
}
|
|
2023
|
+
const byKey = parametersByKey(tag.parameter);
|
|
2024
|
+
const conversionId = scalarMapping(byKey, "conversionId");
|
|
2025
|
+
const conversionLabel = scalarMapping(byKey, "conversionLabel");
|
|
2026
|
+
if (!conversionId || !conversionLabel) {
|
|
2027
|
+
return unknownAssessment(tag, "malformed-google-ads-conversion-tag", triggers, options);
|
|
2028
|
+
}
|
|
2029
|
+
const conversionValue = scalarMapping(byKey, "conversionValue");
|
|
2030
|
+
const transactionId = scalarMapping(byKey, "orderId");
|
|
2031
|
+
const currency = scalarMapping(byKey, "currencyCode");
|
|
2032
|
+
const firingTriggerIds = [...tag.firingTriggerId ?? []];
|
|
2033
|
+
const triggerById = /* @__PURE__ */ new Map();
|
|
2034
|
+
for (const trigger of triggers) {
|
|
2035
|
+
if (typeof trigger.triggerId === "string") triggerById.set(trigger.triggerId, trigger);
|
|
2036
|
+
}
|
|
2037
|
+
const resolved = firingTriggerIds.map((id) => triggerById.get(id)).filter((trigger) => trigger !== void 0);
|
|
2038
|
+
const unresolvedTriggerIds = firingTriggerIds.filter((id) => !triggerById.has(id));
|
|
2039
|
+
const strategy = triggerStrategy(resolved);
|
|
2040
|
+
const eventNames = [...new Set(resolved.flatMap(customEventNames))];
|
|
2041
|
+
const hostFilters = resolved.flatMap((trigger) => hostnameFilters(trigger, options.expectedHostname));
|
|
2042
|
+
const reasons = [];
|
|
2043
|
+
if (tag.paused) reasons.push("tag-paused");
|
|
2044
|
+
if (firingTriggerIds.length === 0) reasons.push("no-firing-trigger");
|
|
2045
|
+
if (unresolvedTriggerIds.length > 0) reasons.push("unresolved-firing-trigger");
|
|
2046
|
+
if (strategy === "url-based") reasons.push("url-based-trigger");
|
|
2047
|
+
if (strategy === "mixed") reasons.push("mixed-trigger-strategies");
|
|
2048
|
+
if (strategy === "custom-event" && eventNames.length === 0) reasons.push("missing-custom-event-name");
|
|
2049
|
+
if (strategy === "custom-event" && options.expectedEventName && !eventNames.includes(options.expectedEventName)) reasons.push("unexpected-custom-event-name");
|
|
2050
|
+
if (hostFilters.length === 0) reasons.push("missing-hostname-filter");
|
|
2051
|
+
if (options.expectedHostname && hostFilters.length > 0 && !hostFilters.some((filter) => filter.matchesExpectedHostname)) reasons.push("unexpected-hostname-filter");
|
|
2052
|
+
if (!conversionValue) reasons.push("missing-conversion-value");
|
|
2053
|
+
if (!transactionId) reasons.push("missing-transaction-id");
|
|
2054
|
+
return {
|
|
2055
|
+
schemaVersion: GTM_GOOGLE_ADS_CONVERSION_ASSESSMENT_SCHEMA_VERSION,
|
|
2056
|
+
recognizerVersion: GTM_GOOGLE_ADS_CONVERSION_RECOGNIZER_VERSION,
|
|
2057
|
+
recognition: { status: "recognized", kind: "google-ads-conversion" },
|
|
2058
|
+
review: { status: reasons.length === 0 ? "pass" : "needs-review", reasons },
|
|
2059
|
+
tag: {
|
|
2060
|
+
id: tag.tagId ?? null,
|
|
2061
|
+
name: tag.name ?? null,
|
|
2062
|
+
type: tag.type ?? null,
|
|
2063
|
+
checksum: checksumJson(tag)
|
|
2064
|
+
},
|
|
2065
|
+
conversion: {
|
|
2066
|
+
id: conversionId,
|
|
2067
|
+
label: conversionLabel,
|
|
2068
|
+
value: conversionValue,
|
|
2069
|
+
transactionId,
|
|
2070
|
+
currency
|
|
2071
|
+
},
|
|
2072
|
+
trigger: {
|
|
2073
|
+
strategy,
|
|
2074
|
+
firingTriggerIds,
|
|
2075
|
+
resolvedTriggerIds: resolved.flatMap((trigger) => trigger.triggerId ? [trigger.triggerId] : []),
|
|
2076
|
+
unresolvedTriggerIds,
|
|
2077
|
+
customEventNames: eventNames,
|
|
2078
|
+
hostnameFilters: hostFilters
|
|
2079
|
+
}
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
// ../integration-google-tag-manager/src/dto-adapter.ts
|
|
2084
|
+
function required(value, label) {
|
|
2085
|
+
if (!value) {
|
|
2086
|
+
throw new GtmApiError(`Tag Manager response is missing ${label}`, 502, {
|
|
2087
|
+
reason: "INVALID_PROVIDER_RESPONSE"
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2090
|
+
return value;
|
|
2091
|
+
}
|
|
2092
|
+
function finalPathSegment(path9) {
|
|
2093
|
+
return path9?.split("/").filter(Boolean).at(-1);
|
|
2094
|
+
}
|
|
2095
|
+
function uniqueStrings(values) {
|
|
2096
|
+
return [...new Set(values.filter((value) => typeof value === "string"))].sort();
|
|
2097
|
+
}
|
|
2098
|
+
function parameterKeys(parameters) {
|
|
2099
|
+
return uniqueStrings((parameters ?? []).map((parameter) => parameter.key));
|
|
2100
|
+
}
|
|
2101
|
+
function parameterValues(parameters) {
|
|
2102
|
+
return (parameters ?? []).flatMap((parameter) => [
|
|
2103
|
+
...typeof parameter.value === "string" ? [parameter.value] : [],
|
|
2104
|
+
...parameterValues(parameter.list),
|
|
2105
|
+
...parameterValues(parameter.map)
|
|
2106
|
+
]);
|
|
2107
|
+
}
|
|
2108
|
+
function referencedVariableIds(tag, variableIdsByName) {
|
|
2109
|
+
const ids = [];
|
|
2110
|
+
for (const value of parameterValues(tag.parameter)) {
|
|
2111
|
+
for (const match of value.matchAll(/\{\{([^{}]+)\}\}/g)) {
|
|
2112
|
+
const name = match[1].trim();
|
|
2113
|
+
if (name) ids.push(...variableIdsByName.get(name) ?? []);
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
return uniqueStrings(ids);
|
|
2117
|
+
}
|
|
2118
|
+
function conditionParameter2(condition, key) {
|
|
2119
|
+
const parameter = condition.parameter?.find((candidate) => candidate.key === key);
|
|
2120
|
+
return typeof parameter?.value === "string" ? parameter.value : null;
|
|
2121
|
+
}
|
|
2122
|
+
function normalizedVariableName2(value) {
|
|
2123
|
+
const trimmed = value.trim();
|
|
2124
|
+
const unwrapped = trimmed.startsWith("{{") && trimmed.endsWith("}}") ? trimmed.slice(2, -2) : trimmed;
|
|
2125
|
+
return unwrapped.trim().toLowerCase();
|
|
2126
|
+
}
|
|
2127
|
+
function safeCustomEventNames(trigger) {
|
|
2128
|
+
const names = [];
|
|
2129
|
+
for (const condition of trigger.customEventFilter ?? []) {
|
|
2130
|
+
const left = conditionParameter2(condition, "arg0");
|
|
2131
|
+
const right = conditionParameter2(condition, "arg1");
|
|
2132
|
+
if (left && right) {
|
|
2133
|
+
const variableName = normalizedVariableName2(left);
|
|
2134
|
+
if (variableName === "_event" || variableName === "event" || variableName === "event name") {
|
|
2135
|
+
names.push(right);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
return uniqueStrings(names);
|
|
2140
|
+
}
|
|
2141
|
+
function conditionKeys(conditions2) {
|
|
2142
|
+
return uniqueStrings((conditions2 ?? []).flatMap((condition) => (condition.parameter ?? []).map((parameter) => parameter.key)));
|
|
2143
|
+
}
|
|
2144
|
+
function triggerConditions(trigger) {
|
|
2145
|
+
return [
|
|
2146
|
+
...trigger.customEventFilter ?? [],
|
|
2147
|
+
...trigger.filter ?? [],
|
|
2148
|
+
...trigger.autoEventFilter ?? []
|
|
2149
|
+
];
|
|
2150
|
+
}
|
|
2151
|
+
function triggerPredicate(trigger) {
|
|
2152
|
+
if (!trigger.triggerId) return null;
|
|
2153
|
+
const eventPredicates = [];
|
|
2154
|
+
const hostnamePredicates = [];
|
|
2155
|
+
let unsupportedConditionCount = 0;
|
|
2156
|
+
for (const condition of triggerConditions(trigger)) {
|
|
2157
|
+
const left = conditionParameter2(condition, "arg0");
|
|
2158
|
+
const right = conditionParameter2(condition, "arg1");
|
|
2159
|
+
const operator = condition.type?.trim() ?? "";
|
|
2160
|
+
if (!left || right === null || !operator || operator.length > 64 || right.length > 2048) {
|
|
2161
|
+
unsupportedConditionCount += 1;
|
|
2162
|
+
continue;
|
|
2163
|
+
}
|
|
2164
|
+
const predicate = {
|
|
2165
|
+
operator,
|
|
2166
|
+
value: right,
|
|
2167
|
+
negated: conditionParameter2(condition, "negate")?.toLowerCase() === "true",
|
|
2168
|
+
ignoreCase: (conditionParameter2(condition, "ignore_case") ?? conditionParameter2(condition, "ignoreCase"))?.toLowerCase() === "true"
|
|
2169
|
+
};
|
|
2170
|
+
const variableName = normalizedVariableName2(left);
|
|
2171
|
+
if (variableName === "_event" || variableName === "event" || variableName === "event name") {
|
|
2172
|
+
eventPredicates.push(predicate);
|
|
2173
|
+
} else if (variableName === "page hostname" || variableName === "_hostname") {
|
|
2174
|
+
hostnamePredicates.push(predicate);
|
|
2175
|
+
} else {
|
|
2176
|
+
unsupportedConditionCount += 1;
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
return {
|
|
2180
|
+
triggerId: trigger.triggerId,
|
|
2181
|
+
triggerType: trigger.type ?? "",
|
|
2182
|
+
eventPredicates,
|
|
2183
|
+
hostnamePredicates,
|
|
2184
|
+
unsupportedConditionCount
|
|
2185
|
+
};
|
|
2186
|
+
}
|
|
2187
|
+
function toGtmAccountDto(account) {
|
|
2188
|
+
return {
|
|
2189
|
+
id: required(account.accountId ?? finalPathSegment(account.path), "account id"),
|
|
2190
|
+
path: account.path ?? "",
|
|
2191
|
+
name: account.name ?? "",
|
|
2192
|
+
shareData: typeof account.shareData === "boolean" ? account.shareData : null
|
|
2193
|
+
};
|
|
2194
|
+
}
|
|
2195
|
+
function toGtmContainerDto(container) {
|
|
2196
|
+
return {
|
|
2197
|
+
accountId: required(container.accountId, "container account id"),
|
|
2198
|
+
id: required(container.containerId ?? finalPathSegment(container.path), "container id"),
|
|
2199
|
+
path: container.path ?? "",
|
|
2200
|
+
name: container.name ?? "",
|
|
2201
|
+
publicId: container.publicId ?? null,
|
|
2202
|
+
domainName: container.domainName?.join(", ") ?? null,
|
|
2203
|
+
usageContexts: [...container.usageContext ?? []]
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
function toGtmWorkspaceDto(workspace) {
|
|
2207
|
+
return {
|
|
2208
|
+
accountId: required(workspace.accountId, "workspace account id"),
|
|
2209
|
+
containerId: required(workspace.containerId, "workspace container id"),
|
|
2210
|
+
id: required(workspace.workspaceId ?? finalPathSegment(workspace.path), "workspace id"),
|
|
2211
|
+
path: workspace.path ?? "",
|
|
2212
|
+
name: workspace.name ?? "",
|
|
2213
|
+
description: workspace.description ?? null,
|
|
2214
|
+
fingerprint: workspace.fingerprint ?? null
|
|
2215
|
+
};
|
|
2216
|
+
}
|
|
2217
|
+
function toGtmContainerVersionDto(version) {
|
|
2218
|
+
return {
|
|
2219
|
+
accountId: required(version.accountId, "container version account id"),
|
|
2220
|
+
containerId: required(version.containerId, "container version container id"),
|
|
2221
|
+
id: required(version.containerVersionId ?? finalPathSegment(version.path), "container version id"),
|
|
2222
|
+
path: version.path ?? "",
|
|
2223
|
+
name: version.name ?? "",
|
|
2224
|
+
description: version.description ?? null,
|
|
2225
|
+
fingerprint: version.fingerprint ?? null,
|
|
2226
|
+
deleted: version.deleted ?? false
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
function toGtmTagDto(tag, fallbackId, variableIdsByName) {
|
|
2230
|
+
return {
|
|
2231
|
+
id: required(tag.tagId ?? fallbackId, "tag id"),
|
|
2232
|
+
name: tag.name ?? "",
|
|
2233
|
+
type: tag.type ?? "",
|
|
2234
|
+
paused: tag.paused ?? false,
|
|
2235
|
+
firingTriggerIds: uniqueStrings(tag.firingTriggerId ?? []),
|
|
2236
|
+
blockingTriggerIds: uniqueStrings(tag.blockingTriggerId ?? []),
|
|
2237
|
+
referencedVariableIds: referencedVariableIds(tag, variableIdsByName),
|
|
2238
|
+
parameterKeys: parameterKeys(tag.parameter),
|
|
2239
|
+
fingerprint: tag.fingerprint ?? null
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
function toGtmTriggerDto(trigger, fallbackId) {
|
|
2243
|
+
return {
|
|
2244
|
+
id: required(trigger.triggerId ?? fallbackId, "trigger id"),
|
|
2245
|
+
name: trigger.name ?? "",
|
|
2246
|
+
type: trigger.type ?? "",
|
|
2247
|
+
customEventNames: safeCustomEventNames(trigger),
|
|
2248
|
+
filterKeys: uniqueStrings([
|
|
2249
|
+
...conditionKeys(trigger.customEventFilter),
|
|
2250
|
+
...conditionKeys(trigger.filter)
|
|
2251
|
+
]),
|
|
2252
|
+
autoEventFilterKeys: conditionKeys(trigger.autoEventFilter),
|
|
2253
|
+
fingerprint: trigger.fingerprint ?? null
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
function dataLayerVariableName(variable) {
|
|
2257
|
+
if (variable.type !== "v") return null;
|
|
2258
|
+
const name = variable.parameter?.find((parameter) => parameter.key === "name")?.value;
|
|
2259
|
+
return typeof name === "string" && name.trim() ? name : null;
|
|
2260
|
+
}
|
|
2261
|
+
function toGtmVariableDto(variable, fallbackId) {
|
|
2262
|
+
return {
|
|
2263
|
+
id: required(variable.variableId ?? fallbackId, "variable id"),
|
|
2264
|
+
name: variable.name ?? "",
|
|
2265
|
+
type: variable.type ?? "",
|
|
2266
|
+
dataLayerVariableName: dataLayerVariableName(variable),
|
|
2267
|
+
parameterKeys: parameterKeys(variable.parameter),
|
|
2268
|
+
fingerprint: variable.fingerprint ?? null
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
function absentMapping() {
|
|
2272
|
+
return { source: "absent", literal: null, variableRef: null };
|
|
2273
|
+
}
|
|
2274
|
+
function unknownMapping() {
|
|
2275
|
+
return { source: "unknown", literal: null, variableRef: null };
|
|
2276
|
+
}
|
|
2277
|
+
function normalizedVariableReference(value) {
|
|
2278
|
+
const trimmed = value.trim();
|
|
2279
|
+
if (!trimmed.startsWith("{{") || !trimmed.endsWith("}}")) return null;
|
|
2280
|
+
const inner = trimmed.slice(2, -2);
|
|
2281
|
+
const normalized = inner.trim();
|
|
2282
|
+
if (!normalized || normalized.length > 128 || inner.includes("{{") || inner.includes("}}") || !/^\w[\w ./-]*$/.test(normalized)) return null;
|
|
2283
|
+
return `{{${normalized}}}`;
|
|
2284
|
+
}
|
|
2285
|
+
function normalizedLiteral(field, value) {
|
|
2286
|
+
const trimmed = value.trim();
|
|
2287
|
+
if (!trimmed) return null;
|
|
2288
|
+
if (field === "conversionId") {
|
|
2289
|
+
const match = /^(?:AW-)?(\d{1,20})$/i.exec(trimmed);
|
|
2290
|
+
return match ? `AW-${match[1]}` : null;
|
|
2291
|
+
}
|
|
2292
|
+
if (field === "conversionLabel") {
|
|
2293
|
+
return /^[\w-]{1,128}$/.test(trimmed) ? trimmed : null;
|
|
2294
|
+
}
|
|
2295
|
+
if (field === "value") {
|
|
2296
|
+
if (!/^(?:0|[1-9]\d{0,11})(?:\.\d{1,6})?$/.test(trimmed)) return null;
|
|
2297
|
+
const [whole, fractional = ""] = trimmed.split(".");
|
|
2298
|
+
const normalizedFractional = fractional.replace(/0+$/, "");
|
|
2299
|
+
return normalizedFractional ? `${whole}.${normalizedFractional}` : whole;
|
|
2300
|
+
}
|
|
2301
|
+
if (field === "currency") {
|
|
2302
|
+
return /^[a-z]{3}$/i.test(trimmed) ? trimmed.toUpperCase() : null;
|
|
2303
|
+
}
|
|
2304
|
+
return null;
|
|
2305
|
+
}
|
|
2306
|
+
function fieldMapping(field, value, source) {
|
|
2307
|
+
if (source === "variable") {
|
|
2308
|
+
const variableRef = normalizedVariableReference(value);
|
|
2309
|
+
return variableRef ? { source: "variable-ref", literal: null, variableRef } : unknownMapping();
|
|
2310
|
+
}
|
|
2311
|
+
if (source !== "literal") return unknownMapping();
|
|
2312
|
+
const literal = normalizedLiteral(field, value);
|
|
2313
|
+
return literal ? { source: "literal", literal, variableRef: null } : unknownMapping();
|
|
2314
|
+
}
|
|
2315
|
+
function toFieldMapping(mapping, field) {
|
|
2316
|
+
return mapping ? fieldMapping(field, mapping.value, mapping.source) : absentMapping();
|
|
2317
|
+
}
|
|
2318
|
+
function safeTagFieldMapping(tag, key, field) {
|
|
2319
|
+
const parameters = (tag.parameter ?? []).filter((parameter) => parameter.key === key);
|
|
2320
|
+
if (parameters.length === 0) return absentMapping();
|
|
2321
|
+
if (parameters.length !== 1) return unknownMapping();
|
|
2322
|
+
const value = parameters[0]?.value;
|
|
2323
|
+
if (typeof value !== "string" || parameters[0]?.list || parameters[0]?.map) return unknownMapping();
|
|
2324
|
+
const variableRef = normalizedVariableReference(value);
|
|
2325
|
+
if (variableRef) return { source: "variable-ref", literal: null, variableRef };
|
|
2326
|
+
if (value.includes("{{") || value.includes("}}") || !value.trim()) return unknownMapping();
|
|
2327
|
+
return fieldMapping(field, value, "literal");
|
|
2328
|
+
}
|
|
2329
|
+
function triggerStrategy2(assessment, triggers) {
|
|
2330
|
+
if (assessment.trigger.unresolvedTriggerIds.length > 0) return "unknown";
|
|
2331
|
+
if (assessment.trigger.strategy === "custom-event") return "custom-event";
|
|
2332
|
+
if (assessment.trigger.strategy !== "url-based") return "unknown";
|
|
2333
|
+
const firingIds = new Set(assessment.trigger.firingTriggerIds);
|
|
2334
|
+
const resolved = triggers.filter((trigger) => trigger.triggerId && firingIds.has(trigger.triggerId));
|
|
2335
|
+
const hasFilters = resolved.some((trigger) => (trigger.customEventFilter?.length ?? 0) > 0 || (trigger.filter?.length ?? 0) > 0 || (trigger.autoEventFilter?.length ?? 0) > 0);
|
|
2336
|
+
return hasFilters ? "filtered" : "all-pages";
|
|
2337
|
+
}
|
|
2338
|
+
function mapReviewReasons(assessment, tag, fields) {
|
|
2339
|
+
const reasons = /* @__PURE__ */ new Set();
|
|
2340
|
+
if (assessment.recognition.status === "unknown") {
|
|
2341
|
+
if (assessment.recognition.reason === "custom-html") reasons.add("custom-html-opaque");
|
|
2342
|
+
else if (assessment.recognition.reason === "custom-template") reasons.add("unsupported-tag-type");
|
|
2343
|
+
else if (assessment.recognition.reason === "unsupported-tag-type") reasons.add("not-google-ads-tag");
|
|
2344
|
+
else {
|
|
2345
|
+
reasons.add("conversion-id-unresolved");
|
|
2346
|
+
reasons.add("conversion-label-unresolved");
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
for (const reason of assessment.review.reasons) {
|
|
2350
|
+
if (reason === "no-firing-trigger" || reason === "unresolved-firing-trigger" || reason === "url-based-trigger" || reason === "mixed-trigger-strategies" || reason === "missing-custom-event-name" || reason === "unexpected-custom-event-name") reasons.add("trigger-unresolved");
|
|
2351
|
+
if (reason === "missing-hostname-filter" || reason === "unexpected-hostname-filter") {
|
|
2352
|
+
reasons.add("hostname-filter-unresolved");
|
|
2353
|
+
}
|
|
2354
|
+
if (reason === "missing-conversion-value") reasons.add("value-mapping-missing");
|
|
2355
|
+
if (reason === "missing-transaction-id") reasons.add("transaction-id-mapping-missing");
|
|
2356
|
+
}
|
|
2357
|
+
if (tag.type === "awct") {
|
|
2358
|
+
if (fields.conversionId.source === "absent" || fields.conversionId.source === "unknown") {
|
|
2359
|
+
reasons.add("conversion-id-unresolved");
|
|
2360
|
+
}
|
|
2361
|
+
if (fields.conversionLabel.source === "absent" || fields.conversionLabel.source === "unknown") {
|
|
2362
|
+
reasons.add("conversion-label-unresolved");
|
|
2363
|
+
}
|
|
2364
|
+
if (fields.value.source === "absent" || fields.value.source === "unknown") {
|
|
2365
|
+
reasons.add("value-mapping-missing");
|
|
2366
|
+
}
|
|
2367
|
+
if (fields.transactionId.source === "absent" || fields.transactionId.source === "unknown") {
|
|
2368
|
+
reasons.add("transaction-id-mapping-missing");
|
|
2369
|
+
}
|
|
2370
|
+
if (fields.currency.source === "absent" || fields.currency.source === "unknown") {
|
|
2371
|
+
reasons.add("currency-mapping-missing");
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
return [...reasons];
|
|
2375
|
+
}
|
|
2376
|
+
function toGtmGoogleAdsTagAssessmentDto(tag, triggers, options = {}, fallbackTagId) {
|
|
2377
|
+
const assessment = recognizeGoogleAdsConversionTag(tag, triggers, options);
|
|
2378
|
+
const knownConversion = assessment.conversion;
|
|
2379
|
+
const knownTag = tag.type === "awct";
|
|
2380
|
+
const fields = {
|
|
2381
|
+
conversionId: gtmGoogleAdsConversionIdMappingDtoSchema.parse(knownConversion ? toFieldMapping(knownConversion.id, "conversionId") : knownTag ? safeTagFieldMapping(tag, "conversionId", "conversionId") : unknownMapping()),
|
|
2382
|
+
conversionLabel: gtmGoogleAdsConversionLabelMappingDtoSchema.parse(knownConversion ? toFieldMapping(knownConversion.label, "conversionLabel") : knownTag ? safeTagFieldMapping(tag, "conversionLabel", "conversionLabel") : unknownMapping()),
|
|
2383
|
+
value: gtmGoogleAdsConversionValueMappingDtoSchema.parse(knownConversion ? toFieldMapping(knownConversion.value, "value") : knownTag ? safeTagFieldMapping(tag, "conversionValue", "value") : unknownMapping()),
|
|
2384
|
+
transactionId: gtmGoogleAdsTransactionIdMappingDtoSchema.parse(knownConversion ? toFieldMapping(knownConversion.transactionId, "transactionId") : knownTag ? safeTagFieldMapping(tag, "orderId", "transactionId") : unknownMapping()),
|
|
2385
|
+
currency: gtmGoogleAdsCurrencyMappingDtoSchema.parse(knownConversion ? toFieldMapping(knownConversion.currency, "currency") : knownTag ? safeTagFieldMapping(tag, "currencyCode", "currency") : unknownMapping())
|
|
2386
|
+
};
|
|
2387
|
+
return {
|
|
2388
|
+
tagId: required(tag.tagId ?? fallbackTagId, "Google Ads assessment tag id"),
|
|
2389
|
+
tagType: tag.type ?? "",
|
|
2390
|
+
recognition: assessment.recognition.status,
|
|
2391
|
+
recognitionReason: assessment.recognition.status === "unknown" ? assessment.recognition.reason : null,
|
|
2392
|
+
...fields,
|
|
2393
|
+
triggerStrategy: triggerStrategy2(assessment, triggers),
|
|
2394
|
+
triggerIds: uniqueStrings(assessment.trigger.firingTriggerIds),
|
|
2395
|
+
triggerPredicates: triggers.filter((trigger) => trigger.triggerId && assessment.trigger.firingTriggerIds.includes(trigger.triggerId)).map(triggerPredicate).filter((predicate) => predicate !== null).sort((left, right) => left.triggerId.localeCompare(right.triggerId)),
|
|
2396
|
+
reviewReasons: mapReviewReasons(assessment, tag, fields)
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
function toGtmConfigurationGraphDto(snapshot, options = {}) {
|
|
2400
|
+
const accountId = required(snapshot.identity.accountId, "snapshot account id");
|
|
2401
|
+
const containerId = required(snapshot.identity.containerId, "snapshot container id");
|
|
2402
|
+
const triggers = snapshot.entities.triggers.map((entity) => entity.raw);
|
|
2403
|
+
const variableIdsByName = /* @__PURE__ */ new Map();
|
|
2404
|
+
for (const entity of snapshot.entities.variables) {
|
|
2405
|
+
const name = entity.raw.name?.trim();
|
|
2406
|
+
if (!name) continue;
|
|
2407
|
+
const id = required(entity.raw.variableId ?? entity.id, "variable id");
|
|
2408
|
+
variableIdsByName.set(name, [...variableIdsByName.get(name) ?? [], id]);
|
|
2409
|
+
}
|
|
2410
|
+
return {
|
|
2411
|
+
accountId,
|
|
2412
|
+
containerId,
|
|
2413
|
+
workspaceId: snapshot.source === "workspace" ? required(snapshot.identity.workspaceId, "snapshot workspace id") : null,
|
|
2414
|
+
tags: snapshot.entities.tags.map((entity) => toGtmTagDto(entity.raw, entity.id, variableIdsByName)),
|
|
2415
|
+
triggers: snapshot.entities.triggers.map((entity) => toGtmTriggerDto(entity.raw, entity.id)),
|
|
2416
|
+
variables: snapshot.entities.variables.map((entity) => toGtmVariableDto(entity.raw, entity.id)),
|
|
2417
|
+
googleAdsTagAssessments: snapshot.entities.tags.map((entity) => toGtmGoogleAdsTagAssessmentDto(entity.raw, triggers, options, entity.id))
|
|
2418
|
+
};
|
|
2419
|
+
}
|
|
2420
|
+
function fetchedAt(options) {
|
|
2421
|
+
return options.fetchedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2422
|
+
}
|
|
2423
|
+
function toGtmLiveContainerGraphDto(snapshot, options = {}) {
|
|
2424
|
+
if (snapshot.source !== "live") {
|
|
2425
|
+
throw new GtmApiError("Live graph adapter requires a live snapshot", 400, {
|
|
2426
|
+
reason: "INVALID_SNAPSHOT_SOURCE"
|
|
2427
|
+
});
|
|
2428
|
+
}
|
|
2429
|
+
const version = snapshot.raw.containerVersion;
|
|
2430
|
+
if (!version) {
|
|
2431
|
+
throw new GtmApiError("Live snapshot is missing its container version", 502, {
|
|
2432
|
+
reason: "INVALID_PROVIDER_RESPONSE"
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
return {
|
|
2436
|
+
source: "live",
|
|
2437
|
+
version: toGtmContainerVersionDto(version),
|
|
2438
|
+
graph: toGtmConfigurationGraphDto(snapshot, options),
|
|
2439
|
+
fetchedAt: fetchedAt(options)
|
|
2440
|
+
};
|
|
2441
|
+
}
|
|
2442
|
+
function toGtmDraftWorkspaceGraphDto(snapshot, options = {}) {
|
|
2443
|
+
if (snapshot.source !== "workspace") {
|
|
2444
|
+
throw new GtmApiError("Draft graph adapter requires a workspace snapshot", 400, {
|
|
2445
|
+
reason: "INVALID_SNAPSHOT_SOURCE"
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
const workspace = snapshot.raw.workspace;
|
|
2449
|
+
if (!workspace) {
|
|
2450
|
+
throw new GtmApiError("Workspace snapshot is missing its workspace", 502, {
|
|
2451
|
+
reason: "INVALID_PROVIDER_RESPONSE"
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
2454
|
+
return {
|
|
2455
|
+
source: "draft",
|
|
2456
|
+
workspace: toGtmWorkspaceDto(workspace),
|
|
2457
|
+
graph: toGtmConfigurationGraphDto(snapshot, options),
|
|
2458
|
+
conflictCount: snapshot.raw.workspaceStatus?.mergeConflict?.length ?? 0,
|
|
2459
|
+
fetchedAt: fetchedAt(options)
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
// src/google-ads-config.ts
|
|
2464
|
+
function ensureConnections(config) {
|
|
2465
|
+
if (!config.googleAds) config.googleAds = {};
|
|
2466
|
+
if (!config.googleAds.connections) config.googleAds.connections = [];
|
|
2467
|
+
return config.googleAds.connections;
|
|
2468
|
+
}
|
|
2469
|
+
function getGoogleAdsAuthConfig(config) {
|
|
2470
|
+
return {
|
|
2471
|
+
clientId: config.googleAds?.clientId ?? config.google?.clientId,
|
|
2472
|
+
clientSecret: config.googleAds?.clientSecret ?? config.google?.clientSecret,
|
|
2473
|
+
developerToken: config.googleAds?.developerToken
|
|
2474
|
+
};
|
|
2475
|
+
}
|
|
2476
|
+
function setGoogleAdsAuthConfig(config, patch) {
|
|
2477
|
+
if (!config.googleAds) config.googleAds = {};
|
|
2478
|
+
if (patch.developerToken !== void 0) config.googleAds.developerToken = patch.developerToken;
|
|
2479
|
+
if (patch.clientId !== void 0) config.googleAds.clientId = patch.clientId;
|
|
2480
|
+
if (patch.clientSecret !== void 0) config.googleAds.clientSecret = patch.clientSecret;
|
|
2481
|
+
}
|
|
2482
|
+
function getGoogleAdsConnection(config, projectId) {
|
|
2483
|
+
return config.googleAds?.connections?.find((entry) => entry.projectId === projectId);
|
|
2484
|
+
}
|
|
2485
|
+
function upsertGoogleAdsConnection(config, connection) {
|
|
2486
|
+
const connections = ensureConnections(config);
|
|
2487
|
+
const index = connections.findIndex((entry) => entry.projectId === connection.projectId);
|
|
2488
|
+
if (index === -1) connections.push(connection);
|
|
2489
|
+
else connections[index] = connection;
|
|
2490
|
+
return connection;
|
|
2491
|
+
}
|
|
2492
|
+
function removeGoogleAdsConnection(config, projectId) {
|
|
2493
|
+
const connections = config.googleAds?.connections;
|
|
2494
|
+
if (!connections?.length) return false;
|
|
2495
|
+
const next = connections.filter((entry) => entry.projectId !== projectId);
|
|
2496
|
+
if (next.length === connections.length) return false;
|
|
2497
|
+
if (!config.googleAds) return false;
|
|
2498
|
+
config.googleAds.connections = next;
|
|
2499
|
+
return true;
|
|
2500
|
+
}
|
|
2501
|
+
function removeLegacyGoogleAdsConnections(config) {
|
|
2502
|
+
const connections = config.googleAds?.connections;
|
|
2503
|
+
if (!connections?.length) return 0;
|
|
2504
|
+
const next = connections.filter((entry) => typeof entry.projectId === "string" && entry.projectId.trim().length > 0);
|
|
2505
|
+
const removed = connections.length - next.length;
|
|
2506
|
+
if (removed > 0 && config.googleAds) config.googleAds.connections = next;
|
|
2507
|
+
return removed;
|
|
2508
|
+
}
|
|
2509
|
+
function removeOrphanedGoogleAdsConnections(config, projectIds) {
|
|
2510
|
+
const connections = config.googleAds?.connections;
|
|
2511
|
+
if (!connections?.length) return 0;
|
|
2512
|
+
const next = connections.filter((entry) => projectIds.has(entry.projectId));
|
|
2513
|
+
const removed = connections.length - next.length;
|
|
2514
|
+
if (removed > 0 && config.googleAds) config.googleAds.connections = next;
|
|
2515
|
+
return removed;
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
// src/gtm-config.ts
|
|
2519
|
+
function ensureConnections2(config) {
|
|
2520
|
+
if (!config.gtm) config.gtm = {};
|
|
2521
|
+
if (!config.gtm.connections) config.gtm.connections = [];
|
|
2522
|
+
return config.gtm.connections;
|
|
2523
|
+
}
|
|
2524
|
+
function getGtmAuthConfig(config) {
|
|
2525
|
+
return {
|
|
2526
|
+
clientId: config.gtm?.clientId ?? config.google?.clientId,
|
|
2527
|
+
clientSecret: config.gtm?.clientSecret ?? config.google?.clientSecret
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
function getGtmConnection(config, projectId) {
|
|
2531
|
+
return config.gtm?.connections?.find((entry) => entry.projectId === projectId);
|
|
2532
|
+
}
|
|
2533
|
+
function upsertGtmConnection(config, connection) {
|
|
2534
|
+
const connections = ensureConnections2(config);
|
|
2535
|
+
const index = connections.findIndex((entry) => entry.projectId === connection.projectId);
|
|
2536
|
+
if (index === -1) connections.push(connection);
|
|
2537
|
+
else connections[index] = connection;
|
|
2538
|
+
return connection;
|
|
2539
|
+
}
|
|
2540
|
+
function removeGtmConnection(config, projectId) {
|
|
2541
|
+
const connections = config.gtm?.connections;
|
|
2542
|
+
if (!connections?.length) return false;
|
|
2543
|
+
const next = connections.filter((entry) => entry.projectId !== projectId);
|
|
2544
|
+
if (next.length === connections.length) return false;
|
|
2545
|
+
if (!config.gtm) return false;
|
|
2546
|
+
config.gtm.connections = next;
|
|
2547
|
+
return true;
|
|
2548
|
+
}
|
|
2549
|
+
function removeLegacyGtmConnections(config) {
|
|
2550
|
+
const connections = config.gtm?.connections;
|
|
2551
|
+
if (!connections?.length) return 0;
|
|
2552
|
+
const next = connections.filter((entry) => typeof entry.projectId === "string" && entry.projectId.trim().length > 0);
|
|
2553
|
+
const removed = connections.length - next.length;
|
|
2554
|
+
if (removed > 0 && config.gtm) config.gtm.connections = next;
|
|
2555
|
+
return removed;
|
|
2556
|
+
}
|
|
2557
|
+
function removeOrphanedGtmConnections(config, projectIds) {
|
|
2558
|
+
const connections = config.gtm?.connections;
|
|
2559
|
+
if (!connections?.length) return 0;
|
|
2560
|
+
const next = connections.filter((entry) => projectIds.has(entry.projectId));
|
|
2561
|
+
const removed = connections.length - next.length;
|
|
2562
|
+
if (removed > 0 && config.gtm) config.gtm.connections = next;
|
|
2563
|
+
return removed;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
// src/google-marketing-runtime.ts
|
|
2567
|
+
var TOKEN_REFRESH_SKEW_MS = 5 * 60 * 1e3;
|
|
2568
|
+
var DEFAULT_LIST_LIMIT = 100;
|
|
2569
|
+
var MAX_LIST_LIMIT = 1e3;
|
|
2570
|
+
var GoogleMarketingRuntimeError = class extends Error {
|
|
2571
|
+
constructor(message, code) {
|
|
2572
|
+
super(message);
|
|
2573
|
+
this.code = code;
|
|
2574
|
+
}
|
|
2575
|
+
code;
|
|
2576
|
+
name = "GoogleMarketingRuntimeError";
|
|
2577
|
+
};
|
|
2578
|
+
function credentialGeneration(connection) {
|
|
2579
|
+
return crypto3.createHash("sha256").update(JSON.stringify({
|
|
2580
|
+
projectId: connection.projectId,
|
|
2581
|
+
credentialGeneration: connection.credentialGeneration ?? null,
|
|
2582
|
+
accessToken: connection.accessToken ?? null,
|
|
2583
|
+
refreshToken: connection.refreshToken ?? null,
|
|
2584
|
+
tokenExpiresAt: connection.tokenExpiresAt ?? null,
|
|
2585
|
+
scopes: connection.scopes ?? [],
|
|
2586
|
+
createdAt: connection.createdAt,
|
|
2587
|
+
updatedAt: connection.updatedAt
|
|
2588
|
+
})).digest("base64url");
|
|
2589
|
+
}
|
|
2590
|
+
function nonEmpty(value, label) {
|
|
2591
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
2592
|
+
throw new GoogleMarketingRuntimeError(`${label} is required`, "INVALID_INPUT");
|
|
2593
|
+
}
|
|
2594
|
+
return value.trim();
|
|
2595
|
+
}
|
|
2596
|
+
function validateProject(project) {
|
|
2597
|
+
return {
|
|
2598
|
+
id: nonEmpty(project.id, "Project ID"),
|
|
2599
|
+
name: nonEmpty(project.name, "Project name")
|
|
2600
|
+
};
|
|
2601
|
+
}
|
|
2602
|
+
function validatedListLimit(value) {
|
|
2603
|
+
const limit = value ?? DEFAULT_LIST_LIMIT;
|
|
2604
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > MAX_LIST_LIMIT) {
|
|
2605
|
+
throw new GoogleMarketingRuntimeError(
|
|
2606
|
+
`List limit must be an integer between 1 and ${MAX_LIST_LIMIT}`,
|
|
2607
|
+
"INVALID_INPUT"
|
|
2608
|
+
);
|
|
2609
|
+
}
|
|
2610
|
+
return limit;
|
|
2611
|
+
}
|
|
2612
|
+
function resourceId2(value, label) {
|
|
2613
|
+
const id = value.split("/").filter(Boolean).at(-1);
|
|
2614
|
+
if (!id) throw new GoogleMarketingRuntimeError(`${label} is invalid`, "INVALID_INPUT");
|
|
2615
|
+
return id;
|
|
2616
|
+
}
|
|
2617
|
+
function canonicalGtmAccountId(value) {
|
|
2618
|
+
const accountId = canonicalizeGtmAccountId(value);
|
|
2619
|
+
if (!accountId) {
|
|
2620
|
+
throw new GoogleMarketingRuntimeError("GTM account ID is invalid", "INVALID_INPUT");
|
|
2621
|
+
}
|
|
2622
|
+
return accountId;
|
|
2623
|
+
}
|
|
2624
|
+
function canonicalGtmContainerId(value, accountId) {
|
|
2625
|
+
const containerId = canonicalizeGtmContainerId(value, accountId);
|
|
2626
|
+
if (!containerId) {
|
|
2627
|
+
throw new GoogleMarketingRuntimeError("GTM container ID is invalid or belongs to another account", "INVALID_INPUT");
|
|
2628
|
+
}
|
|
2629
|
+
return containerId;
|
|
2630
|
+
}
|
|
2631
|
+
function accountPath(accountId) {
|
|
2632
|
+
return `accounts/${canonicalGtmAccountId(accountId)}`;
|
|
2633
|
+
}
|
|
2634
|
+
function containerPath(accountId, containerId) {
|
|
2635
|
+
const canonicalAccountId = canonicalGtmAccountId(accountId);
|
|
2636
|
+
return `accounts/${canonicalAccountId}/containers/${canonicalGtmContainerId(containerId, canonicalAccountId)}`;
|
|
2637
|
+
}
|
|
2638
|
+
function workspacePath(accountId, containerId, workspaceId) {
|
|
2639
|
+
const selection = canonicalizeGtmResourceSelection({ accountId, containerId, workspaceId });
|
|
2640
|
+
if (!selection?.workspaceId) {
|
|
2641
|
+
throw new GoogleMarketingRuntimeError("GTM workspace ID is invalid or belongs to another container", "INVALID_INPUT");
|
|
2642
|
+
}
|
|
2643
|
+
return `accounts/${selection.accountId}/containers/${selection.containerId}/workspaces/${selection.workspaceId}`;
|
|
2644
|
+
}
|
|
2645
|
+
function normalizedCustomerStatus(status) {
|
|
2646
|
+
switch (status?.toUpperCase()) {
|
|
2647
|
+
case "ENABLED":
|
|
2648
|
+
return GoogleAdsCustomerStatuses.enabled;
|
|
2649
|
+
case "SUSPENDED":
|
|
2650
|
+
return GoogleAdsCustomerStatuses.suspended;
|
|
2651
|
+
case "CLOSED":
|
|
2652
|
+
return GoogleAdsCustomerStatuses.closed;
|
|
2653
|
+
case "CANCELED":
|
|
2654
|
+
return GoogleAdsCustomerStatuses.canceled;
|
|
2655
|
+
case "UNSPECIFIED":
|
|
2656
|
+
return GoogleAdsCustomerStatuses.unspecified;
|
|
2657
|
+
default:
|
|
2658
|
+
return GoogleAdsCustomerStatuses.unknown;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
function accessibleCustomerDto(customer) {
|
|
2662
|
+
return googleAdsAccessibleCustomerDtoSchema.parse({
|
|
2663
|
+
resourceName: customer.resourceName,
|
|
2664
|
+
customerId: customer.id,
|
|
2665
|
+
parentCustomerId: null,
|
|
2666
|
+
descriptiveName: customer.descriptiveName ?? null,
|
|
2667
|
+
currencyCode: customer.currencyCode ?? null,
|
|
2668
|
+
timeZone: customer.timeZone ?? null,
|
|
2669
|
+
manager: customer.manager ?? false,
|
|
2670
|
+
hidden: false,
|
|
2671
|
+
testAccount: customer.testAccount ?? false,
|
|
2672
|
+
level: 0,
|
|
2673
|
+
status: normalizedCustomerStatus(customer.status)
|
|
2674
|
+
});
|
|
2675
|
+
}
|
|
2676
|
+
function accessibleCustomerClientDto(customer, loginCustomerId) {
|
|
2677
|
+
const level = Number(customer.level ?? "0");
|
|
2678
|
+
if (!Number.isSafeInteger(level) || level < 0) {
|
|
2679
|
+
throw new GoogleMarketingRuntimeError(
|
|
2680
|
+
"Google Ads returned an invalid customer hierarchy level",
|
|
2681
|
+
"INVALID_INPUT"
|
|
2682
|
+
);
|
|
2683
|
+
}
|
|
2684
|
+
return googleAdsAccessibleCustomerDtoSchema.parse({
|
|
2685
|
+
resourceName: customer.clientCustomer || `customers/${customer.id}`,
|
|
2686
|
+
customerId: customer.id,
|
|
2687
|
+
// Google Ads exposes the selected login-manager root and hierarchy depth,
|
|
2688
|
+
// but not a direct parent edge in this query. Retain the root as the
|
|
2689
|
+
// account context required to read any discovered child honestly.
|
|
2690
|
+
parentCustomerId: customer.id === loginCustomerId ? null : loginCustomerId,
|
|
2691
|
+
descriptiveName: customer.descriptiveName ?? null,
|
|
2692
|
+
currencyCode: customer.currencyCode ?? null,
|
|
2693
|
+
timeZone: customer.timeZone ?? null,
|
|
2694
|
+
manager: customer.manager ?? false,
|
|
2695
|
+
hidden: customer.hidden ?? false,
|
|
2696
|
+
testAccount: customer.testAccount ?? false,
|
|
2697
|
+
level,
|
|
2698
|
+
status: normalizedCustomerStatus(customer.status)
|
|
2699
|
+
});
|
|
2700
|
+
}
|
|
2701
|
+
function campaignStatus(status) {
|
|
2702
|
+
switch (status.toUpperCase()) {
|
|
2703
|
+
case "ENABLED":
|
|
2704
|
+
return GoogleAdsCampaignStatuses.enabled;
|
|
2705
|
+
case "PAUSED":
|
|
2706
|
+
return GoogleAdsCampaignStatuses.paused;
|
|
2707
|
+
case "REMOVED":
|
|
2708
|
+
return GoogleAdsCampaignStatuses.removed;
|
|
2709
|
+
default:
|
|
2710
|
+
return GoogleAdsCampaignStatuses.unknown;
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
function conversionActionStatus(status) {
|
|
2714
|
+
switch (status.toUpperCase()) {
|
|
2715
|
+
case "ENABLED":
|
|
2716
|
+
return GoogleAdsConversionActionStatuses.enabled;
|
|
2717
|
+
case "HIDDEN":
|
|
2718
|
+
return GoogleAdsConversionActionStatuses.hidden;
|
|
2719
|
+
case "REMOVED":
|
|
2720
|
+
return GoogleAdsConversionActionStatuses.removed;
|
|
2721
|
+
default:
|
|
2722
|
+
return GoogleAdsConversionActionStatuses.unknown;
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
function campaignDto(campaign) {
|
|
2726
|
+
return {
|
|
2727
|
+
id: campaign.id,
|
|
2728
|
+
resourceName: campaign.resourceName,
|
|
2729
|
+
name: campaign.name,
|
|
2730
|
+
status: campaignStatus(campaign.status),
|
|
2731
|
+
advertisingChannelType: campaign.advertisingChannelType ?? null,
|
|
2732
|
+
biddingStrategyType: campaign.biddingStrategyType ?? null
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
function conversionActionDto(action) {
|
|
2736
|
+
return {
|
|
2737
|
+
id: action.id,
|
|
2738
|
+
resourceName: action.resourceName,
|
|
2739
|
+
name: action.name,
|
|
2740
|
+
status: conversionActionStatus(action.status),
|
|
2741
|
+
category: action.category,
|
|
2742
|
+
origin: action.origin,
|
|
2743
|
+
primaryForGoal: action.primaryForGoal ?? false,
|
|
2744
|
+
includeInConversionsMetric: action.includeInConversionsMetric ?? false
|
|
2745
|
+
};
|
|
2746
|
+
}
|
|
2747
|
+
function inventoryDto(customerId, fetchedAt2, campaigns, goals) {
|
|
2748
|
+
return {
|
|
2749
|
+
customerId,
|
|
2750
|
+
fetchedAt: fetchedAt2,
|
|
2751
|
+
campaigns: campaigns.map(campaignDto),
|
|
2752
|
+
conversionActions: goals.conversionActions.map((row) => conversionActionDto(row.conversionAction)),
|
|
2753
|
+
customerConversionGoals: goals.customerGoals.map((row) => ({
|
|
2754
|
+
category: row.customerConversionGoal.category,
|
|
2755
|
+
origin: row.customerConversionGoal.origin,
|
|
2756
|
+
biddable: row.customerConversionGoal.biddable ?? false
|
|
2757
|
+
})),
|
|
2758
|
+
campaignConversionGoals: goals.campaignGoals.map((row) => ({
|
|
2759
|
+
campaignId: row.campaign.id,
|
|
2760
|
+
category: row.campaignConversionGoal.category,
|
|
2761
|
+
origin: row.campaignConversionGoal.origin,
|
|
2762
|
+
biddable: row.campaignConversionGoal.biddable ?? false
|
|
2763
|
+
})),
|
|
2764
|
+
campaignConversionGoalsComplete: goals.campaignGoalsComplete === true,
|
|
2765
|
+
customConversionGoals: goals.customGoals.map((row) => ({
|
|
2766
|
+
id: row.customConversionGoal.id,
|
|
2767
|
+
name: row.customConversionGoal.name,
|
|
2768
|
+
conversionActionIds: (row.customConversionGoal.conversionActions ?? []).map((name) => resourceId2(name, "Conversion action resource name"))
|
|
2769
|
+
})),
|
|
2770
|
+
campaignGoalConfigurations: goals.campaignConfigs.map((row) => {
|
|
2771
|
+
const level = row.conversionGoalCampaignConfig.goalConfigLevel;
|
|
2772
|
+
if (level !== "CUSTOMER" && level !== "CAMPAIGN") {
|
|
2773
|
+
throw new GoogleMarketingRuntimeError(
|
|
2774
|
+
"Google Ads returned an unsupported campaign goal configuration level",
|
|
2775
|
+
"INVALID_INPUT"
|
|
2776
|
+
);
|
|
2777
|
+
}
|
|
2778
|
+
return {
|
|
2779
|
+
campaignId: row.campaign.id,
|
|
2780
|
+
goalConfigLevel: level === "CAMPAIGN" ? GoogleAdsGoalConfigurationLevels.campaign : GoogleAdsGoalConfigurationLevels.customer,
|
|
2781
|
+
customGoalId: row.conversionGoalCampaignConfig.customConversionGoal ? resourceId2(
|
|
2782
|
+
row.conversionGoalCampaignConfig.customConversionGoal,
|
|
2783
|
+
"Custom conversion goal resource name"
|
|
2784
|
+
) : null
|
|
2785
|
+
};
|
|
2786
|
+
})
|
|
2787
|
+
};
|
|
2788
|
+
}
|
|
2789
|
+
function nonnegativeInteger(value, label) {
|
|
2790
|
+
const parsed = Number(value ?? "0");
|
|
2791
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) {
|
|
2792
|
+
throw new GoogleMarketingRuntimeError(`Google Ads returned an invalid ${label}`, "INVALID_INPUT");
|
|
2793
|
+
}
|
|
2794
|
+
return parsed;
|
|
2795
|
+
}
|
|
2796
|
+
function conversionValueMicros(metrics) {
|
|
2797
|
+
if (metrics.conversionsValue === void 0) return null;
|
|
2798
|
+
const value = Math.round(metrics.conversionsValue * 1e6);
|
|
2799
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
2800
|
+
throw new GoogleMarketingRuntimeError(
|
|
2801
|
+
"Google Ads returned an invalid conversion value",
|
|
2802
|
+
"INVALID_INPUT"
|
|
2803
|
+
);
|
|
2804
|
+
}
|
|
2805
|
+
return value;
|
|
2806
|
+
}
|
|
2807
|
+
function campaignMetricDto(row) {
|
|
2808
|
+
return {
|
|
2809
|
+
campaignId: row.campaign.id,
|
|
2810
|
+
date: row.segments.date,
|
|
2811
|
+
impressions: nonnegativeInteger(row.metrics.impressions, "impression count"),
|
|
2812
|
+
clicks: nonnegativeInteger(row.metrics.clicks, "click count"),
|
|
2813
|
+
costMicros: nonnegativeInteger(row.metrics.costMicros, "cost"),
|
|
2814
|
+
conversions: row.metrics.conversions ?? 0,
|
|
2815
|
+
conversionValueMicros: conversionValueMicros(row.metrics)
|
|
2816
|
+
};
|
|
2817
|
+
}
|
|
2818
|
+
function defaultMetricsQuery(now, campaigns, timeZone) {
|
|
2819
|
+
const campaignIds = campaigns.map((campaign) => campaign.id).sort((left, right) => left.localeCompare(right));
|
|
2820
|
+
const selected = campaignIds.slice(0, GOOGLE_ADS_CAMPAIGN_METRICS_MAX_CAMPAIGNS);
|
|
2821
|
+
if (selected.length === 0) return { query: null, inventoryTruncated: false };
|
|
2822
|
+
const customerTimeZone = typeof timeZone === "string" && timeZone.trim() !== "" ? timeZone : "UTC";
|
|
2823
|
+
const endDate = formatIsoDateInTimeZone(now.toISOString(), customerTimeZone);
|
|
2824
|
+
return {
|
|
2825
|
+
query: googleAdsCampaignMetricsQuerySchema.parse({
|
|
2826
|
+
campaignIds: selected,
|
|
2827
|
+
startDate: shiftIsoCalendarDate(endDate, -(GOOGLE_ADS_CAMPAIGN_METRICS_MAX_DAYS - 1)),
|
|
2828
|
+
endDate
|
|
2829
|
+
}),
|
|
2830
|
+
inventoryTruncated: campaignIds.length > selected.length
|
|
2831
|
+
};
|
|
2832
|
+
}
|
|
2833
|
+
function countRedactedProviderValues(value) {
|
|
2834
|
+
if (value === null || typeof value !== "object") return 0;
|
|
2835
|
+
if (Array.isArray(value)) {
|
|
2836
|
+
let total = 0;
|
|
2837
|
+
for (const item of value) total += countRedactedProviderValues(item);
|
|
2838
|
+
return total;
|
|
2839
|
+
}
|
|
2840
|
+
return Object.entries(value).reduce((total, [key, item]) => {
|
|
2841
|
+
const own = key === "value" || key === "globalSiteTag" || key === "eventSnippet" ? 1 : 0;
|
|
2842
|
+
return total + own + countRedactedProviderValues(item);
|
|
2843
|
+
}, 0);
|
|
2844
|
+
}
|
|
2845
|
+
function rawMetadata(value) {
|
|
2846
|
+
return {
|
|
2847
|
+
// The provider clients intentionally do not expose exact response bytes.
|
|
2848
|
+
// A hash of normalized objects would not be an honest raw-body checksum.
|
|
2849
|
+
rawPayloadSha256: null,
|
|
2850
|
+
rawPayloadBytes: null,
|
|
2851
|
+
redactedFieldCount: countRedactedProviderValues(value)
|
|
2852
|
+
};
|
|
2853
|
+
}
|
|
2854
|
+
var ConfigCredentialStore = class {
|
|
2855
|
+
#config;
|
|
2856
|
+
#saveConfigPatch;
|
|
2857
|
+
#now;
|
|
2858
|
+
#refresh;
|
|
2859
|
+
#refreshes = /* @__PURE__ */ new Map();
|
|
2860
|
+
constructor(options) {
|
|
2861
|
+
this.#config = options.config;
|
|
2862
|
+
this.#saveConfigPatch = options.saveConfigPatch;
|
|
2863
|
+
this.#now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2864
|
+
this.#refresh = options.refreshAccessToken ?? refreshAccessToken;
|
|
2865
|
+
}
|
|
2866
|
+
async getCredential(project, provider) {
|
|
2867
|
+
const normalizedProject = validateProject(project);
|
|
2868
|
+
const accessToken = await this.#accessToken(normalizedProject, provider);
|
|
2869
|
+
return { accessToken };
|
|
2870
|
+
}
|
|
2871
|
+
async #accessToken(project, provider) {
|
|
2872
|
+
const connection = provider === GoogleMarketingProviders["google-ads"] ? getGoogleAdsConnection(this.#config, project.id) : getGtmConnection(this.#config, project.id);
|
|
2873
|
+
if (!connection) {
|
|
2874
|
+
throw new GoogleMarketingRuntimeError(
|
|
2875
|
+
`${provider === GoogleMarketingProviders["google-ads"] ? "Google Ads" : "GTM"} is not connected for this project`,
|
|
2876
|
+
"CONNECTION_NOT_FOUND"
|
|
2877
|
+
);
|
|
2878
|
+
}
|
|
2879
|
+
if (this.#hasFreshAccessToken(connection)) return connection.accessToken;
|
|
2880
|
+
const key = `${provider}\0${project.id}\0${credentialGeneration(connection)}`;
|
|
2881
|
+
const pending = this.#refreshes.get(key);
|
|
2882
|
+
if (pending) return pending;
|
|
2883
|
+
const refresh = this.#refreshConnection(project, provider, connection);
|
|
2884
|
+
this.#refreshes.set(key, refresh);
|
|
2885
|
+
try {
|
|
2886
|
+
return await refresh;
|
|
2887
|
+
} finally {
|
|
2888
|
+
if (this.#refreshes.get(key) === refresh) this.#refreshes.delete(key);
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
#hasFreshAccessToken(connection) {
|
|
2892
|
+
if (!connection.accessToken || !connection.tokenExpiresAt) return false;
|
|
2893
|
+
const expiresAt = Date.parse(connection.tokenExpiresAt);
|
|
2894
|
+
return Number.isFinite(expiresAt) && this.#now().getTime() < expiresAt - TOKEN_REFRESH_SKEW_MS;
|
|
2895
|
+
}
|
|
2896
|
+
#connection(provider, projectId) {
|
|
2897
|
+
return provider === GoogleMarketingProviders["google-ads"] ? getGoogleAdsConnection(this.#config, projectId) : getGtmConnection(this.#config, projectId);
|
|
2898
|
+
}
|
|
2899
|
+
#connectionHasGeneration(provider, projectId, expectedGeneration) {
|
|
2900
|
+
const current = this.#connection(provider, projectId);
|
|
2901
|
+
return current !== void 0 && credentialGeneration(current) === expectedGeneration;
|
|
2902
|
+
}
|
|
2903
|
+
#restoreConnection(provider, connection) {
|
|
2904
|
+
if (provider === GoogleMarketingProviders["google-ads"]) {
|
|
2905
|
+
upsertGoogleAdsConnection(this.#config, connection);
|
|
2906
|
+
} else {
|
|
2907
|
+
upsertGtmConnection(this.#config, connection);
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
async #refreshConnection(project, provider, connection) {
|
|
2911
|
+
if (!connection.refreshToken) {
|
|
2912
|
+
throw new GoogleMarketingRuntimeError(
|
|
2913
|
+
"Google OAuth access has expired and no refresh token is available",
|
|
2914
|
+
"TOKEN_REFRESH_REQUIRED"
|
|
2915
|
+
);
|
|
2916
|
+
}
|
|
2917
|
+
const auth = provider === GoogleMarketingProviders["google-ads"] ? getGoogleAdsAuthConfig(this.#config) : getGtmAuthConfig(this.#config);
|
|
2918
|
+
if (!auth.clientId || !auth.clientSecret) {
|
|
2919
|
+
throw new GoogleMarketingRuntimeError(
|
|
2920
|
+
"Google OAuth client credentials are not configured",
|
|
2921
|
+
"NOT_CONFIGURED"
|
|
2922
|
+
);
|
|
2923
|
+
}
|
|
2924
|
+
const generationBeforeRefresh = credentialGeneration(connection);
|
|
2925
|
+
const tokens = await this.#refresh(auth.clientId, auth.clientSecret, connection.refreshToken);
|
|
2926
|
+
if (typeof tokens.access_token !== "string" || tokens.access_token.trim() === "" || !Number.isFinite(tokens.expires_in) || tokens.expires_in <= 0) {
|
|
2927
|
+
throw new GoogleMarketingRuntimeError(
|
|
2928
|
+
"Google OAuth returned an invalid token response",
|
|
2929
|
+
"INVALID_TOKEN_RESPONSE"
|
|
2930
|
+
);
|
|
2931
|
+
}
|
|
2932
|
+
const now = this.#now();
|
|
2933
|
+
const patch = {
|
|
2934
|
+
accessToken: tokens.access_token,
|
|
2935
|
+
refreshToken: tokens.refresh_token ?? connection.refreshToken,
|
|
2936
|
+
tokenExpiresAt: new Date(now.getTime() + tokens.expires_in * 1e3).toISOString(),
|
|
2937
|
+
...tokens.scope ? { scopes: tokens.scope.split(/\s+/).filter(Boolean) } : {},
|
|
2938
|
+
updatedAt: now.toISOString()
|
|
2939
|
+
};
|
|
2940
|
+
if (!this.#connectionHasGeneration(provider, project.id, generationBeforeRefresh)) {
|
|
2941
|
+
throw new GoogleMarketingRuntimeError(
|
|
2942
|
+
`${provider === GoogleMarketingProviders["google-ads"] ? "Google Ads" : "GTM"} connection changed during token refresh`,
|
|
2943
|
+
"CONNECTION_NOT_FOUND"
|
|
2944
|
+
);
|
|
2945
|
+
}
|
|
2946
|
+
const refreshedConnection = { ...connection, ...patch };
|
|
2947
|
+
const refreshedGeneration = credentialGeneration(refreshedConnection);
|
|
2948
|
+
this.#restoreConnection(provider, refreshedConnection);
|
|
2949
|
+
try {
|
|
2950
|
+
await this.#saveConfigPatch(
|
|
2951
|
+
provider === GoogleMarketingProviders["google-ads"] ? { googleAds: this.#config.googleAds } : { gtm: this.#config.gtm }
|
|
2952
|
+
);
|
|
2953
|
+
} catch (error) {
|
|
2954
|
+
if (this.#connectionHasGeneration(provider, project.id, refreshedGeneration)) {
|
|
2955
|
+
this.#restoreConnection(provider, connection);
|
|
2956
|
+
}
|
|
2957
|
+
throw error;
|
|
2958
|
+
}
|
|
2959
|
+
return tokens.access_token;
|
|
2960
|
+
}
|
|
2961
|
+
};
|
|
2962
|
+
var DefaultGoogleMarketingRuntime = class {
|
|
2963
|
+
#config;
|
|
2964
|
+
#credentials;
|
|
2965
|
+
#env;
|
|
2966
|
+
#fetch;
|
|
2967
|
+
#now;
|
|
2968
|
+
#randomUUID;
|
|
2969
|
+
constructor(options) {
|
|
2970
|
+
this.#config = options.config;
|
|
2971
|
+
this.#credentials = new ConfigCredentialStore(options);
|
|
2972
|
+
this.#env = options.env ?? process.env;
|
|
2973
|
+
this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
2974
|
+
this.#now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2975
|
+
this.#randomUUID = options.randomUUID ?? crypto3.randomUUID;
|
|
2976
|
+
}
|
|
2977
|
+
async listGoogleAdsCustomers(project, options = {}) {
|
|
2978
|
+
const normalizedProject = validateProject(project);
|
|
2979
|
+
const client = await this.#adsClient(normalizedProject, options.loginCustomerId);
|
|
2980
|
+
const result = await client.listAccessibleCustomerDetails({ maxCustomers: options.maxCustomers });
|
|
2981
|
+
const fetchedAt2 = this.#now().toISOString();
|
|
2982
|
+
const customersById = new Map(
|
|
2983
|
+
result.data.customers.map((customer) => {
|
|
2984
|
+
const dto = accessibleCustomerDto(customer);
|
|
2985
|
+
return [dto.customerId, dto];
|
|
2986
|
+
})
|
|
2987
|
+
);
|
|
2988
|
+
let hierarchyTruncated = false;
|
|
2989
|
+
for (const root of result.data.customers) {
|
|
2990
|
+
if (!root.manager) continue;
|
|
2991
|
+
const remaining = GOOGLE_ADS_MAX_CUSTOMER_CLIENTS - customersById.size;
|
|
2992
|
+
if (remaining <= 0) {
|
|
2993
|
+
hierarchyTruncated = true;
|
|
2994
|
+
break;
|
|
2995
|
+
}
|
|
2996
|
+
try {
|
|
2997
|
+
const hierarchy = await client.getCustomerClients(root.id, { maxCustomers: remaining });
|
|
2998
|
+
hierarchyTruncated ||= hierarchy.data.truncated;
|
|
2999
|
+
for (const customer of hierarchy.data.customerClients) {
|
|
3000
|
+
const dto = accessibleCustomerClientDto(customer, root.id);
|
|
3001
|
+
const existing = customersById.get(dto.customerId);
|
|
3002
|
+
if (!existing || dto.level < existing.level) customersById.set(dto.customerId, dto);
|
|
3003
|
+
}
|
|
3004
|
+
} catch {
|
|
3005
|
+
hierarchyTruncated = true;
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
const customers = [...customersById.values()].sort((left, right) => left.level - right.level || left.customerId.localeCompare(right.customerId));
|
|
3009
|
+
return googleAdsAccessibleCustomersResponseSchema.parse({
|
|
3010
|
+
customers,
|
|
3011
|
+
totalAccessible: customers.length,
|
|
3012
|
+
truncated: result.data.truncated || result.data.failures.length > 0 || hierarchyTruncated,
|
|
3013
|
+
selection: {
|
|
3014
|
+
loginCustomerId: options.loginCustomerId ? normalizeGoogleAdsCustomerId(options.loginCustomerId, "Login customer ID") : null,
|
|
3015
|
+
customerId: null,
|
|
3016
|
+
selectedAt: null
|
|
3017
|
+
},
|
|
3018
|
+
fetchedAt: fetchedAt2
|
|
3019
|
+
});
|
|
3020
|
+
}
|
|
3021
|
+
async getGoogleAdsCustomer(project, input) {
|
|
3022
|
+
const normalizedProject = validateProject(project);
|
|
3023
|
+
const client = await this.#adsClient(normalizedProject, input.loginCustomerId);
|
|
3024
|
+
const result = await client.getCustomerDetails(input.customerId);
|
|
3025
|
+
return accessibleCustomerDto(result.data);
|
|
3026
|
+
}
|
|
3027
|
+
async listGtmAccounts(project, options = {}) {
|
|
3028
|
+
const limit = validatedListLimit(options.maxResults);
|
|
3029
|
+
const client = await this.#gtmClient(validateProject(project));
|
|
3030
|
+
const accounts = (await client.listAccounts()).map(toGtmAccountDto);
|
|
3031
|
+
return gtmAccountsResponseSchema.parse({
|
|
3032
|
+
accounts: accounts.slice(0, limit),
|
|
3033
|
+
totalAccessible: accounts.length,
|
|
3034
|
+
truncated: accounts.length > limit,
|
|
3035
|
+
fetchedAt: this.#now().toISOString()
|
|
3036
|
+
});
|
|
3037
|
+
}
|
|
3038
|
+
async listGtmContainers(project, accountId, options = {}) {
|
|
3039
|
+
const limit = validatedListLimit(options.maxResults);
|
|
3040
|
+
const normalizedAccountId = canonicalGtmAccountId(accountId);
|
|
3041
|
+
const client = await this.#gtmClient(validateProject(project));
|
|
3042
|
+
const containers = (await client.listContainers(accountPath(normalizedAccountId))).map(toGtmContainerDto);
|
|
3043
|
+
return gtmContainerListResponseSchema.parse({
|
|
3044
|
+
accountId: normalizedAccountId,
|
|
3045
|
+
containers: containers.slice(0, limit),
|
|
3046
|
+
totalAccessible: containers.length,
|
|
3047
|
+
truncated: containers.length > limit,
|
|
3048
|
+
fetchedAt: this.#now().toISOString()
|
|
3049
|
+
});
|
|
3050
|
+
}
|
|
3051
|
+
async listGtmWorkspaces(project, accountId, containerId, options = {}) {
|
|
3052
|
+
const limit = validatedListLimit(options.maxResults);
|
|
3053
|
+
const normalizedAccountId = canonicalGtmAccountId(accountId);
|
|
3054
|
+
const normalizedContainerId = canonicalGtmContainerId(containerId, normalizedAccountId);
|
|
3055
|
+
const client = await this.#gtmClient(validateProject(project));
|
|
3056
|
+
const workspaces = (await client.listWorkspaces(
|
|
3057
|
+
containerPath(normalizedAccountId, normalizedContainerId)
|
|
3058
|
+
)).map(toGtmWorkspaceDto);
|
|
3059
|
+
return gtmWorkspaceListResponseSchema.parse({
|
|
3060
|
+
accountId: normalizedAccountId,
|
|
3061
|
+
containerId: normalizedContainerId,
|
|
3062
|
+
workspaces: workspaces.slice(0, limit),
|
|
3063
|
+
totalAccessible: workspaces.length,
|
|
3064
|
+
truncated: workspaces.length > limit,
|
|
3065
|
+
fetchedAt: this.#now().toISOString()
|
|
3066
|
+
});
|
|
3067
|
+
}
|
|
3068
|
+
async syncGoogleAds(input) {
|
|
3069
|
+
const project = validateProject(input.project);
|
|
3070
|
+
const context = this.#snapshotContext(project, input.connectionId, input.runId);
|
|
3071
|
+
const customerId = normalizeGoogleAdsCustomerId(input.selection.customerId);
|
|
3072
|
+
const suppliedMetricsQuery = input.metricsQuery ? googleAdsCampaignMetricsQuerySchema.parse(input.metricsQuery) : void 0;
|
|
3073
|
+
const client = await this.#adsClient(project, input.selection.loginCustomerId);
|
|
3074
|
+
const [customerResult, campaignResult, goalResult] = await Promise.all([
|
|
3075
|
+
client.getCustomerDetails(customerId),
|
|
3076
|
+
client.getCampaigns(customerId, { limit: GOOGLE_ADS_MAX_RESULT_ROWS }),
|
|
3077
|
+
client.getConversionGoals(customerId)
|
|
3078
|
+
]);
|
|
3079
|
+
const capturedAt = this.#now().toISOString();
|
|
3080
|
+
const accessibleCustomersData = googleAdsAccessibleCustomersResponseSchema.parse({
|
|
3081
|
+
customers: [accessibleCustomerDto(customerResult.data)],
|
|
3082
|
+
totalAccessible: 1,
|
|
3083
|
+
truncated: false,
|
|
3084
|
+
selection: {
|
|
3085
|
+
loginCustomerId: input.selection.loginCustomerId ? normalizeGoogleAdsCustomerId(input.selection.loginCustomerId, "Login customer ID") : null,
|
|
3086
|
+
customerId,
|
|
3087
|
+
selectedAt: input.selection.selectedAt ?? null
|
|
3088
|
+
},
|
|
3089
|
+
fetchedAt: capturedAt
|
|
3090
|
+
});
|
|
3091
|
+
const accessibleCustomersPayload = {
|
|
3092
|
+
kind: GoogleAdsSnapshotKinds["accessible-customers"],
|
|
3093
|
+
data: accessibleCustomersData
|
|
3094
|
+
};
|
|
3095
|
+
const accessibleCustomersSnapshot = this.#googleAdsSnapshot(
|
|
3096
|
+
context,
|
|
3097
|
+
accessibleCustomersPayload,
|
|
3098
|
+
customerId,
|
|
3099
|
+
customerResult.data,
|
|
3100
|
+
capturedAt
|
|
3101
|
+
);
|
|
3102
|
+
const inventory = inventoryDto(
|
|
3103
|
+
customerId,
|
|
3104
|
+
capturedAt,
|
|
3105
|
+
campaignResult.data.map((row) => row.campaign),
|
|
3106
|
+
goalResult.data
|
|
3107
|
+
);
|
|
3108
|
+
const inventoryPayload = {
|
|
3109
|
+
kind: GoogleAdsSnapshotKinds.inventory,
|
|
3110
|
+
data: inventory
|
|
3111
|
+
};
|
|
3112
|
+
const inventoryRaw = {
|
|
3113
|
+
campaigns: campaignResult.data,
|
|
3114
|
+
conversionGoals: goalResult.data
|
|
3115
|
+
};
|
|
3116
|
+
const inventorySnapshot = this.#googleAdsSnapshot(
|
|
3117
|
+
context,
|
|
3118
|
+
inventoryPayload,
|
|
3119
|
+
customerId,
|
|
3120
|
+
inventoryRaw,
|
|
3121
|
+
capturedAt
|
|
3122
|
+
);
|
|
3123
|
+
const effectiveGoalGraph = deriveGoogleAdsEffectiveGoalGraph(inventory);
|
|
3124
|
+
const defaults = defaultMetricsQuery(this.#now(), inventory.campaigns, customerResult.data.timeZone);
|
|
3125
|
+
const metricsQuery = suppliedMetricsQuery ?? defaults.query;
|
|
3126
|
+
if (!metricsQuery) {
|
|
3127
|
+
return {
|
|
3128
|
+
accessibleCustomers: accessibleCustomersSnapshot,
|
|
3129
|
+
inventory: inventorySnapshot,
|
|
3130
|
+
metrics: null,
|
|
3131
|
+
effectiveGoalGraph
|
|
3132
|
+
};
|
|
3133
|
+
}
|
|
3134
|
+
const metricResult = await client.getDailyCampaignMetrics(customerId, {
|
|
3135
|
+
...metricsQuery,
|
|
3136
|
+
limit: GOOGLE_ADS_CAMPAIGN_METRICS_MAX_ROWS + 1
|
|
3137
|
+
});
|
|
3138
|
+
const metricRows = metricResult.data.map(campaignMetricDto);
|
|
3139
|
+
const response = {
|
|
3140
|
+
query: metricsQuery,
|
|
3141
|
+
rows: metricRows.slice(0, GOOGLE_ADS_CAMPAIGN_METRICS_MAX_ROWS),
|
|
3142
|
+
truncated: metricRows.length > GOOGLE_ADS_CAMPAIGN_METRICS_MAX_ROWS || suppliedMetricsQuery === void 0 && defaults.inventoryTruncated,
|
|
3143
|
+
fetchedAt: this.#now().toISOString()
|
|
3144
|
+
};
|
|
3145
|
+
const metricsPayload = {
|
|
3146
|
+
kind: GoogleAdsSnapshotKinds["campaign-metrics"],
|
|
3147
|
+
data: response
|
|
3148
|
+
};
|
|
3149
|
+
const metricsSnapshot = this.#googleAdsSnapshot(
|
|
3150
|
+
context,
|
|
3151
|
+
metricsPayload,
|
|
3152
|
+
customerId,
|
|
3153
|
+
metricResult.data,
|
|
3154
|
+
response.fetchedAt
|
|
3155
|
+
);
|
|
3156
|
+
return {
|
|
3157
|
+
accessibleCustomers: accessibleCustomersSnapshot,
|
|
3158
|
+
inventory: inventorySnapshot,
|
|
3159
|
+
metrics: metricsSnapshot,
|
|
3160
|
+
effectiveGoalGraph
|
|
3161
|
+
};
|
|
3162
|
+
}
|
|
3163
|
+
async syncGtm(input) {
|
|
3164
|
+
const project = validateProject(input.project);
|
|
3165
|
+
const context = this.#snapshotContext(project, input.connectionId, input.runId);
|
|
3166
|
+
const selected = canonicalizeGtmResourceSelection({
|
|
3167
|
+
accountId: input.selection.accountId,
|
|
3168
|
+
containerId: input.selection.containerId,
|
|
3169
|
+
...input.selection.workspaceId === void 0 || input.selection.workspaceId === null ? {} : { workspaceId: input.selection.workspaceId }
|
|
3170
|
+
});
|
|
3171
|
+
if (!selected) {
|
|
3172
|
+
throw new GoogleMarketingRuntimeError("GTM resource selection is invalid", "INVALID_INPUT");
|
|
3173
|
+
}
|
|
3174
|
+
const selectedAccountId = selected.accountId;
|
|
3175
|
+
const selectedContainerId = selected.containerId;
|
|
3176
|
+
const selectedWorkspaceId = selected.workspaceId ?? null;
|
|
3177
|
+
const client = await this.#gtmClient(project);
|
|
3178
|
+
const accounts = await client.listAccounts();
|
|
3179
|
+
const accountDtos = accounts.map(toGtmAccountDto);
|
|
3180
|
+
const account = accountDtos.find((candidate) => candidate.id === selectedAccountId);
|
|
3181
|
+
if (!account) {
|
|
3182
|
+
throw new GoogleMarketingRuntimeError("Selected GTM account was not found", "RESOURCE_NOT_FOUND");
|
|
3183
|
+
}
|
|
3184
|
+
const selectedContainerPath = containerPath(selectedAccountId, selectedContainerId);
|
|
3185
|
+
const [containers, workspaces] = await Promise.all([
|
|
3186
|
+
client.listContainers(accountPath(selectedAccountId)),
|
|
3187
|
+
client.listWorkspaces(selectedContainerPath)
|
|
3188
|
+
]);
|
|
3189
|
+
const containerDtos = containers.map(toGtmContainerDto);
|
|
3190
|
+
const container = containerDtos.find((candidate) => candidate.id === selectedContainerId);
|
|
3191
|
+
if (!container) {
|
|
3192
|
+
throw new GoogleMarketingRuntimeError("Selected GTM container was not found", "RESOURCE_NOT_FOUND");
|
|
3193
|
+
}
|
|
3194
|
+
const workspaceDtos = workspaces.map(toGtmWorkspaceDto);
|
|
3195
|
+
if (workspaceDtos.length > MAX_LIST_LIMIT) {
|
|
3196
|
+
throw new GoogleMarketingRuntimeError(
|
|
3197
|
+
`GTM workspace inventory exceeds the ${MAX_LIST_LIMIT}-workspace safety limit`,
|
|
3198
|
+
"INVALID_INPUT"
|
|
3199
|
+
);
|
|
3200
|
+
}
|
|
3201
|
+
const selectedWorkspace = selectedWorkspaceId ? workspaceDtos.find((candidate) => candidate.id === selectedWorkspaceId) : void 0;
|
|
3202
|
+
if (selectedWorkspaceId && !selectedWorkspace) {
|
|
3203
|
+
throw new GoogleMarketingRuntimeError("Selected GTM workspace was not found", "RESOURCE_NOT_FOUND");
|
|
3204
|
+
}
|
|
3205
|
+
const snapshotReads = [
|
|
3206
|
+
client.getLiveSnapshot(selectedContainerPath)
|
|
3207
|
+
];
|
|
3208
|
+
if (selectedWorkspaceId) {
|
|
3209
|
+
snapshotReads.push(client.getWorkspaceSnapshot(
|
|
3210
|
+
workspacePath(selectedAccountId, selectedContainerId, selectedWorkspaceId)
|
|
3211
|
+
));
|
|
3212
|
+
}
|
|
3213
|
+
const snapshots = await Promise.all(snapshotReads);
|
|
3214
|
+
const capturedAt = this.#now().toISOString();
|
|
3215
|
+
const adapterOptions = {
|
|
3216
|
+
fetchedAt: capturedAt,
|
|
3217
|
+
...input.expectedEventName ? { expectedEventName: input.expectedEventName } : {},
|
|
3218
|
+
...input.expectedHostname ? { expectedHostname: input.expectedHostname } : {}
|
|
3219
|
+
};
|
|
3220
|
+
const live = toGtmLiveContainerGraphDto(snapshots[0], adapterOptions);
|
|
3221
|
+
const draft = snapshots[1] ? toGtmDraftWorkspaceGraphDto(snapshots[1], adapterOptions) : null;
|
|
3222
|
+
const payload = {
|
|
3223
|
+
kind: GtmSnapshotKinds.container,
|
|
3224
|
+
data: {
|
|
3225
|
+
account,
|
|
3226
|
+
container,
|
|
3227
|
+
workspaces: workspaceDtos,
|
|
3228
|
+
live,
|
|
3229
|
+
draft,
|
|
3230
|
+
fetchedAt: capturedAt
|
|
3231
|
+
}
|
|
3232
|
+
};
|
|
3233
|
+
const raw = { accounts, containers, workspaces, snapshots };
|
|
3234
|
+
return this.#gtmSnapshot(
|
|
3235
|
+
context,
|
|
3236
|
+
payload,
|
|
3237
|
+
selectedAccountId,
|
|
3238
|
+
selectedContainerId,
|
|
3239
|
+
selectedWorkspaceId,
|
|
3240
|
+
raw,
|
|
3241
|
+
capturedAt
|
|
3242
|
+
);
|
|
3243
|
+
}
|
|
3244
|
+
async #adsClient(project, loginCustomerId) {
|
|
3245
|
+
const { accessToken } = await this.#credentials.getCredential(
|
|
3246
|
+
project,
|
|
3247
|
+
GoogleMarketingProviders["google-ads"]
|
|
3248
|
+
);
|
|
3249
|
+
const auth = getGoogleAdsAuthConfig(this.#config);
|
|
3250
|
+
const developerToken = this.#env.GOOGLE_ADS_DEVELOPER_TOKEN?.trim() || auth.developerToken?.trim();
|
|
3251
|
+
if (!developerToken) {
|
|
3252
|
+
throw new GoogleMarketingRuntimeError(
|
|
3253
|
+
"Google Ads developer token is not configured",
|
|
3254
|
+
"NOT_CONFIGURED"
|
|
3255
|
+
);
|
|
3256
|
+
}
|
|
3257
|
+
return new GoogleAdsClient({
|
|
3258
|
+
accessToken,
|
|
3259
|
+
developerToken,
|
|
3260
|
+
...loginCustomerId ? { loginCustomerId } : {}
|
|
3261
|
+
}, { fetch: this.#fetch });
|
|
3262
|
+
}
|
|
3263
|
+
async #gtmClient(project) {
|
|
3264
|
+
const { accessToken } = await this.#credentials.getCredential(
|
|
3265
|
+
project,
|
|
3266
|
+
GoogleMarketingProviders.gtm
|
|
3267
|
+
);
|
|
3268
|
+
return createGoogleTagManagerClient(accessToken, { fetch: this.#fetch });
|
|
3269
|
+
}
|
|
3270
|
+
#snapshotContext(project, connectionId, runId) {
|
|
3271
|
+
return {
|
|
3272
|
+
project,
|
|
3273
|
+
connectionId: nonEmpty(connectionId, "Connection ID"),
|
|
3274
|
+
runId: nonEmpty(runId, "Run ID")
|
|
3275
|
+
};
|
|
3276
|
+
}
|
|
3277
|
+
#googleAdsSnapshot(context, payload, customerId, raw, capturedAt) {
|
|
3278
|
+
return googleAdsRawSnapshotDtoSchema.parse({
|
|
3279
|
+
metadata: {
|
|
3280
|
+
id: this.#randomUUID(),
|
|
3281
|
+
projectId: context.project.id,
|
|
3282
|
+
connectionId: context.connectionId,
|
|
3283
|
+
runId: context.runId,
|
|
3284
|
+
kind: payload.kind,
|
|
3285
|
+
customerId,
|
|
3286
|
+
payloadChecksum: checksumJson(payload),
|
|
3287
|
+
...rawMetadata(raw),
|
|
3288
|
+
capturedAt,
|
|
3289
|
+
createdAt: capturedAt
|
|
3290
|
+
},
|
|
3291
|
+
payload
|
|
3292
|
+
});
|
|
3293
|
+
}
|
|
3294
|
+
#gtmSnapshot(context, payload, accountId, containerId, workspaceId, raw, capturedAt) {
|
|
3295
|
+
return gtmRawSnapshotDtoSchema.parse({
|
|
3296
|
+
metadata: {
|
|
3297
|
+
id: this.#randomUUID(),
|
|
3298
|
+
projectId: context.project.id,
|
|
3299
|
+
connectionId: context.connectionId,
|
|
3300
|
+
runId: context.runId,
|
|
3301
|
+
kind: payload.kind,
|
|
3302
|
+
accountId,
|
|
3303
|
+
containerId,
|
|
3304
|
+
workspaceId,
|
|
3305
|
+
payloadChecksum: checksumJson(payload),
|
|
3306
|
+
...rawMetadata(raw),
|
|
3307
|
+
capturedAt,
|
|
3308
|
+
createdAt: capturedAt
|
|
3309
|
+
},
|
|
3310
|
+
payload
|
|
3311
|
+
});
|
|
3312
|
+
}
|
|
3313
|
+
};
|
|
3314
|
+
function createGoogleMarketingCredentialStore(options) {
|
|
3315
|
+
return new ConfigCredentialStore(options);
|
|
3316
|
+
}
|
|
3317
|
+
function createGoogleMarketingRuntime(options) {
|
|
3318
|
+
return new DefaultGoogleMarketingRuntime(options);
|
|
3319
|
+
}
|
|
3320
|
+
|
|
579
3321
|
// src/server.ts
|
|
580
3322
|
import { createRequire as createRequire4 } from "module";
|
|
581
|
-
import
|
|
3323
|
+
import crypto26 from "crypto";
|
|
582
3324
|
import fs7 from "fs";
|
|
583
3325
|
import path8 from "path";
|
|
584
3326
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
585
|
-
import { and as
|
|
3327
|
+
import { and as and19, eq as eq25 } from "drizzle-orm";
|
|
586
3328
|
import Fastify from "fastify";
|
|
587
3329
|
import os4 from "os";
|
|
588
3330
|
|
|
@@ -2977,7 +5719,7 @@ function getPlacesConfig(config) {
|
|
|
2977
5719
|
}
|
|
2978
5720
|
|
|
2979
5721
|
// src/google-config.ts
|
|
2980
|
-
function
|
|
5722
|
+
function ensureConnections3(config) {
|
|
2981
5723
|
if (!config.google) config.google = {};
|
|
2982
5724
|
if (!config.google.connections) config.google.connections = [];
|
|
2983
5725
|
return config.google.connections;
|
|
@@ -3006,7 +5748,7 @@ function getGoogleConnection(config, domain, connectionType) {
|
|
|
3006
5748
|
return (config.google?.connections ?? []).find((connection) => connection.domain === domain && connection.connectionType === connectionType);
|
|
3007
5749
|
}
|
|
3008
5750
|
function upsertGoogleConnection(config, connection) {
|
|
3009
|
-
const connections =
|
|
5751
|
+
const connections = ensureConnections3(config);
|
|
3010
5752
|
const index = connections.findIndex((entry) => entry.domain === connection.domain && entry.connectionType === connection.connectionType);
|
|
3011
5753
|
const normalized = {
|
|
3012
5754
|
...connection,
|
|
@@ -3051,7 +5793,7 @@ function removeGoogleConnection(config, domain, connectionType) {
|
|
|
3051
5793
|
}
|
|
3052
5794
|
|
|
3053
5795
|
// src/ga4-config.ts
|
|
3054
|
-
function
|
|
5796
|
+
function ensureConnections4(config) {
|
|
3055
5797
|
if (!config.ga4) config.ga4 = {};
|
|
3056
5798
|
if (!config.ga4.connections) config.ga4.connections = [];
|
|
3057
5799
|
return config.ga4.connections;
|
|
@@ -3060,7 +5802,7 @@ function getGa4Connection(config, projectName) {
|
|
|
3060
5802
|
return (config.ga4?.connections ?? []).find((c) => c.projectName === projectName);
|
|
3061
5803
|
}
|
|
3062
5804
|
function upsertGa4Connection(config, connection) {
|
|
3063
|
-
const connections =
|
|
5805
|
+
const connections = ensureConnections4(config);
|
|
3064
5806
|
const index = connections.findIndex((c) => c.projectName === connection.projectName);
|
|
3065
5807
|
if (index === -1) {
|
|
3066
5808
|
connections.push(connection);
|
|
@@ -3083,7 +5825,7 @@ function removeGa4Connection(config, projectName) {
|
|
|
3083
5825
|
}
|
|
3084
5826
|
|
|
3085
5827
|
// src/cloud-run-config.ts
|
|
3086
|
-
function
|
|
5828
|
+
function ensureConnections5(config) {
|
|
3087
5829
|
if (!config.cloudRun) config.cloudRun = {};
|
|
3088
5830
|
if (!config.cloudRun.connections) config.cloudRun.connections = [];
|
|
3089
5831
|
return config.cloudRun.connections;
|
|
@@ -3092,7 +5834,7 @@ function getCloudRunConnection(config, projectName) {
|
|
|
3092
5834
|
return (config.cloudRun?.connections ?? []).find((c) => c.projectName === projectName);
|
|
3093
5835
|
}
|
|
3094
5836
|
function upsertCloudRunConnection(config, connection) {
|
|
3095
|
-
const connections =
|
|
5837
|
+
const connections = ensureConnections5(config);
|
|
3096
5838
|
const index = connections.findIndex((c) => c.projectName === connection.projectName);
|
|
3097
5839
|
if (index === -1) {
|
|
3098
5840
|
connections.push(connection);
|
|
@@ -3115,7 +5857,7 @@ function removeCloudRunConnection(config, projectName) {
|
|
|
3115
5857
|
}
|
|
3116
5858
|
|
|
3117
5859
|
// src/wordpress-traffic-config.ts
|
|
3118
|
-
function
|
|
5860
|
+
function ensureConnections6(config) {
|
|
3119
5861
|
if (!config.wordpressTraffic) config.wordpressTraffic = {};
|
|
3120
5862
|
if (!config.wordpressTraffic.connections) config.wordpressTraffic.connections = [];
|
|
3121
5863
|
return config.wordpressTraffic.connections;
|
|
@@ -3124,7 +5866,7 @@ function getWordpressTrafficConnection(config, projectName) {
|
|
|
3124
5866
|
return (config.wordpressTraffic?.connections ?? []).find((c) => c.projectName === projectName);
|
|
3125
5867
|
}
|
|
3126
5868
|
function upsertWordpressTrafficConnection(config, connection) {
|
|
3127
|
-
const connections =
|
|
5869
|
+
const connections = ensureConnections6(config);
|
|
3128
5870
|
const index = connections.findIndex((c) => c.projectName === connection.projectName);
|
|
3129
5871
|
if (index === -1) {
|
|
3130
5872
|
connections.push(connection);
|
|
@@ -3147,7 +5889,7 @@ function removeWordpressTrafficConnection(config, projectName) {
|
|
|
3147
5889
|
}
|
|
3148
5890
|
|
|
3149
5891
|
// src/vercel-traffic-config.ts
|
|
3150
|
-
function
|
|
5892
|
+
function ensureConnections7(config) {
|
|
3151
5893
|
if (!config.vercelTraffic) config.vercelTraffic = {};
|
|
3152
5894
|
if (!config.vercelTraffic.connections) config.vercelTraffic.connections = [];
|
|
3153
5895
|
return config.vercelTraffic.connections;
|
|
@@ -3156,7 +5898,7 @@ function getVercelTrafficConnection(config, projectName) {
|
|
|
3156
5898
|
return (config.vercelTraffic?.connections ?? []).find((c) => c.projectName === projectName);
|
|
3157
5899
|
}
|
|
3158
5900
|
function upsertVercelTrafficConnection(config, connection) {
|
|
3159
|
-
const connections =
|
|
5901
|
+
const connections = ensureConnections7(config);
|
|
3160
5902
|
const index = connections.findIndex((c) => c.projectName === connection.projectName);
|
|
3161
5903
|
if (index === -1) {
|
|
3162
5904
|
connections.push(connection);
|
|
@@ -3183,7 +5925,7 @@ function unsupportedDeliveryMode(deliveryMode) {
|
|
|
3183
5925
|
const label = typeof deliveryMode === "string" ? ` "${deliveryMode}"` : "";
|
|
3184
5926
|
return new Error(`Unsupported Cloudflare traffic delivery mode${label}`);
|
|
3185
5927
|
}
|
|
3186
|
-
function
|
|
5928
|
+
function ensureConnections8(config) {
|
|
3187
5929
|
if (!config.cloudflareTraffic) config.cloudflareTraffic = {};
|
|
3188
5930
|
if (!config.cloudflareTraffic.connections) config.cloudflareTraffic.connections = [];
|
|
3189
5931
|
return config.cloudflareTraffic.connections;
|
|
@@ -3213,7 +5955,7 @@ function getCloudflareTrafficConnectionBySourceId(config, sourceId) {
|
|
|
3213
5955
|
}
|
|
3214
5956
|
function upsertCloudflareTrafficConnection(config, connection) {
|
|
3215
5957
|
normalizeCloudflareTrafficConnection(connection);
|
|
3216
|
-
const connections =
|
|
5958
|
+
const connections = ensureConnections8(config);
|
|
3217
5959
|
const index = connections.findIndex((c) => c.sourceId === connection.sourceId);
|
|
3218
5960
|
if (index === -1) {
|
|
3219
5961
|
connections.push(connection);
|
|
@@ -3252,7 +5994,7 @@ function buildCloudflareIngestUrlTemplate(config) {
|
|
|
3252
5994
|
}
|
|
3253
5995
|
|
|
3254
5996
|
// src/wordpress-config.ts
|
|
3255
|
-
function
|
|
5997
|
+
function ensureConnections9(config) {
|
|
3256
5998
|
if (!config.wordpress) config.wordpress = {};
|
|
3257
5999
|
if (!config.wordpress.connections) config.wordpress.connections = [];
|
|
3258
6000
|
return config.wordpress.connections;
|
|
@@ -3269,7 +6011,7 @@ function getWordpressConnection(config, projectName) {
|
|
|
3269
6011
|
return (config.wordpress?.connections ?? []).find((connection) => connection.projectName === projectName);
|
|
3270
6012
|
}
|
|
3271
6013
|
function upsertWordpressConnection(config, connection) {
|
|
3272
|
-
const connections =
|
|
6014
|
+
const connections = ensureConnections9(config);
|
|
3273
6015
|
const normalized = normalizeConnection(connection);
|
|
3274
6016
|
const index = connections.findIndex((entry) => entry.projectName === connection.projectName);
|
|
3275
6017
|
if (index === -1) {
|
|
@@ -3303,14 +6045,14 @@ function removeWordpressConnection(config, projectName) {
|
|
|
3303
6045
|
}
|
|
3304
6046
|
|
|
3305
6047
|
// src/job-runner.ts
|
|
3306
|
-
import
|
|
6048
|
+
import crypto6 from "crypto";
|
|
3307
6049
|
import fs2 from "fs";
|
|
3308
6050
|
import path3 from "path";
|
|
3309
6051
|
import os3 from "os";
|
|
3310
6052
|
import { and as and2, eq as eq2, inArray, ne, sql as sql2 } from "drizzle-orm";
|
|
3311
6053
|
|
|
3312
6054
|
// src/run-telemetry.ts
|
|
3313
|
-
import
|
|
6055
|
+
import crypto4 from "crypto";
|
|
3314
6056
|
function extractRegistrableHost(input) {
|
|
3315
6057
|
if (!input) return null;
|
|
3316
6058
|
const trimmed = input.trim();
|
|
@@ -3330,7 +6072,7 @@ function extractRegistrableHost(input) {
|
|
|
3330
6072
|
function hashDomain(input) {
|
|
3331
6073
|
const host = extractRegistrableHost(input);
|
|
3332
6074
|
if (!host) return null;
|
|
3333
|
-
return
|
|
6075
|
+
return crypto4.createHash("sha256").update(host).digest("hex");
|
|
3334
6076
|
}
|
|
3335
6077
|
function buildRunCompletedProps(input) {
|
|
3336
6078
|
const totalMs = input.phases?.total_ms ?? Date.now() - input.startTime;
|
|
@@ -3415,7 +6157,7 @@ function getSharedProviderExecutionGate(providerName, maxConcurrency, maxPerMinu
|
|
|
3415
6157
|
}
|
|
3416
6158
|
|
|
3417
6159
|
// src/usage-quota.ts
|
|
3418
|
-
import
|
|
6160
|
+
import crypto5 from "crypto";
|
|
3419
6161
|
import { and, eq, lte, sql } from "drizzle-orm";
|
|
3420
6162
|
function getCurrentUsageDay() {
|
|
3421
6163
|
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
@@ -3432,7 +6174,7 @@ function reserveDailyQueryQuota(db, input) {
|
|
|
3432
6174
|
)).run();
|
|
3433
6175
|
if (reserveExisting().changes === 1) return { reserved: true, used: 0 };
|
|
3434
6176
|
const inserted = tx.insert(usageCounters).values({
|
|
3435
|
-
id:
|
|
6177
|
+
id: crypto5.randomUUID(),
|
|
3436
6178
|
scope: input.scope,
|
|
3437
6179
|
period: input.period,
|
|
3438
6180
|
metric: "queries",
|
|
@@ -3846,7 +6588,7 @@ var JobRunner = class {
|
|
|
3846
6588
|
);
|
|
3847
6589
|
let screenshotRelPath = null;
|
|
3848
6590
|
if (raw.screenshotPath && fs2.existsSync(raw.screenshotPath)) {
|
|
3849
|
-
const snapshotId =
|
|
6591
|
+
const snapshotId = crypto6.randomUUID();
|
|
3850
6592
|
const screenshotDir = path3.join(os3.homedir(), ".canonry", "screenshots", runId);
|
|
3851
6593
|
if (!fs2.existsSync(screenshotDir)) fs2.mkdirSync(screenshotDir, { recursive: true });
|
|
3852
6594
|
const destPath = path3.join(screenshotDir, `${snapshotId}.png`);
|
|
@@ -3889,7 +6631,7 @@ var JobRunner = class {
|
|
|
3889
6631
|
}).run();
|
|
3890
6632
|
} else {
|
|
3891
6633
|
this.db.insert(querySnapshots).values({
|
|
3892
|
-
id:
|
|
6634
|
+
id: crypto6.randomUUID(),
|
|
3893
6635
|
runId,
|
|
3894
6636
|
queryId: q.id,
|
|
3895
6637
|
queryText: q.query,
|
|
@@ -4001,7 +6743,7 @@ var JobRunner = class {
|
|
|
4001
6743
|
competitorDomains,
|
|
4002
6744
|
allBrandNames
|
|
4003
6745
|
);
|
|
4004
|
-
const snapshotId =
|
|
6746
|
+
const snapshotId = crypto6.randomUUID();
|
|
4005
6747
|
let screenshotRelPath = null;
|
|
4006
6748
|
if (raw.screenshotPath && fs2.existsSync(raw.screenshotPath)) {
|
|
4007
6749
|
const screenshotDir = path3.join(os3.homedir(), ".canonry", "screenshots", runId);
|
|
@@ -4237,7 +6979,7 @@ var JobRunner = class {
|
|
|
4237
6979
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4238
6980
|
const period = now.slice(0, 10);
|
|
4239
6981
|
this.db.insert(usageCounters).values({
|
|
4240
|
-
id:
|
|
6982
|
+
id: crypto6.randomUUID(),
|
|
4241
6983
|
scope,
|
|
4242
6984
|
period,
|
|
4243
6985
|
metric,
|
|
@@ -4339,11 +7081,11 @@ function maybeShowActivationNotice(io = {}) {
|
|
|
4339
7081
|
}
|
|
4340
7082
|
|
|
4341
7083
|
// src/gsc-sync.ts
|
|
4342
|
-
import
|
|
7084
|
+
import crypto8 from "crypto";
|
|
4343
7085
|
import { eq as eq4, and as and4, sql as sql3 } from "drizzle-orm";
|
|
4344
7086
|
|
|
4345
7087
|
// src/gsc-coverage-snapshot.ts
|
|
4346
|
-
import
|
|
7088
|
+
import crypto7 from "crypto";
|
|
4347
7089
|
import { and as and3, desc, eq as eq3, gte } from "drizzle-orm";
|
|
4348
7090
|
var COVERAGE_IMPRESSION_WINDOW_DAYS = 30;
|
|
4349
7091
|
function pageKey(raw) {
|
|
@@ -4405,7 +7147,7 @@ function writeCoverageSnapshot(db, projectId, runId, opts = {}) {
|
|
|
4405
7147
|
const snapshotDate = now.toISOString().split("T")[0];
|
|
4406
7148
|
db.delete(gscCoverageSnapshots).where(and3(eq3(gscCoverageSnapshots.projectId, projectId), eq3(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
4407
7149
|
db.insert(gscCoverageSnapshots).values({
|
|
4408
|
-
id:
|
|
7150
|
+
id: crypto7.randomUUID(),
|
|
4409
7151
|
projectId,
|
|
4410
7152
|
syncRunId: runId,
|
|
4411
7153
|
date: snapshotDate,
|
|
@@ -4484,7 +7226,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
4484
7226
|
for (const row of batch) {
|
|
4485
7227
|
const [query, page, country, device, date] = row.keys;
|
|
4486
7228
|
db.insert(gscSearchData).values({
|
|
4487
|
-
id:
|
|
7229
|
+
id: crypto8.randomUUID(),
|
|
4488
7230
|
projectId,
|
|
4489
7231
|
syncRunId: runId,
|
|
4490
7232
|
date: date ?? "",
|
|
@@ -4516,7 +7258,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
4516
7258
|
for (const row of totalRows) {
|
|
4517
7259
|
const [date] = row.keys;
|
|
4518
7260
|
db.insert(gscDailyTotals).values({
|
|
4519
|
-
id:
|
|
7261
|
+
id: crypto8.randomUUID(),
|
|
4520
7262
|
projectId,
|
|
4521
7263
|
date: date ?? "",
|
|
4522
7264
|
clicks: row.clicks,
|
|
@@ -4565,7 +7307,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
4565
7307
|
const [date, query] = row.keys;
|
|
4566
7308
|
if (!date || !query) continue;
|
|
4567
7309
|
db.insert(gscQueryDailyTotals).values({
|
|
4568
|
-
id:
|
|
7310
|
+
id: crypto8.randomUUID(),
|
|
4569
7311
|
projectId,
|
|
4570
7312
|
date,
|
|
4571
7313
|
query,
|
|
@@ -4590,7 +7332,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
4590
7332
|
}
|
|
4591
7333
|
|
|
4592
7334
|
// src/gbp-sync.ts
|
|
4593
|
-
import
|
|
7335
|
+
import crypto9 from "crypto";
|
|
4594
7336
|
import { eq as eq5, and as and5, desc as desc2, inArray as inArray2, lt } from "drizzle-orm";
|
|
4595
7337
|
var MS_PER_DAY = 864e5;
|
|
4596
7338
|
var log3 = createLogger("GbpSync");
|
|
@@ -4715,7 +7457,7 @@ async function executeGbpSync(db, runId, projectId, opts) {
|
|
|
4715
7457
|
tx.delete(gbpDailyMetrics).where(and5(eq5(gbpDailyMetrics.projectId, projectId), eq5(gbpDailyMetrics.locationName, loc.locationName))).run();
|
|
4716
7458
|
for (const row of metricRows) {
|
|
4717
7459
|
tx.insert(gbpDailyMetrics).values({
|
|
4718
|
-
id:
|
|
7460
|
+
id: crypto9.randomUUID(),
|
|
4719
7461
|
projectId,
|
|
4720
7462
|
locationName: loc.locationName,
|
|
4721
7463
|
date: row.date,
|
|
@@ -4727,7 +7469,7 @@ async function executeGbpSync(db, runId, projectId, opts) {
|
|
|
4727
7469
|
tx.delete(gbpKeywordImpressions).where(and5(eq5(gbpKeywordImpressions.projectId, projectId), eq5(gbpKeywordImpressions.locationName, loc.locationName))).run();
|
|
4728
7470
|
for (const row of keywordRows) {
|
|
4729
7471
|
tx.insert(gbpKeywordImpressions).values({
|
|
4730
|
-
id:
|
|
7472
|
+
id: crypto9.randomUUID(),
|
|
4731
7473
|
projectId,
|
|
4732
7474
|
locationName: loc.locationName,
|
|
4733
7475
|
periodStart: monthKey(keywordsStart),
|
|
@@ -4748,7 +7490,7 @@ async function executeGbpSync(db, runId, projectId, opts) {
|
|
|
4748
7490
|
for (const { month, rows } of monthlyKeywordResults) {
|
|
4749
7491
|
for (const row of rows) {
|
|
4750
7492
|
tx.insert(gbpKeywordMonthly).values({
|
|
4751
|
-
id:
|
|
7493
|
+
id: crypto9.randomUUID(),
|
|
4752
7494
|
projectId,
|
|
4753
7495
|
locationName: loc.locationName,
|
|
4754
7496
|
month,
|
|
@@ -4769,7 +7511,7 @@ async function executeGbpSync(db, runId, projectId, opts) {
|
|
|
4769
7511
|
tx.delete(gbpPlaceActions).where(and5(eq5(gbpPlaceActions.projectId, projectId), eq5(gbpPlaceActions.locationName, loc.locationName))).run();
|
|
4770
7512
|
for (const row of placeActionRows) {
|
|
4771
7513
|
tx.insert(gbpPlaceActions).values({
|
|
4772
|
-
id:
|
|
7514
|
+
id: crypto9.randomUUID(),
|
|
4773
7515
|
projectId,
|
|
4774
7516
|
locationName: loc.locationName,
|
|
4775
7517
|
placeActionLinkName: row.placeActionLinkName,
|
|
@@ -4782,7 +7524,7 @@ async function executeGbpSync(db, runId, projectId, opts) {
|
|
|
4782
7524
|
}
|
|
4783
7525
|
if (lodging !== null && lodgingChanged) {
|
|
4784
7526
|
tx.insert(gbpLodgingSnapshots).values({
|
|
4785
|
-
id:
|
|
7527
|
+
id: crypto9.randomUUID(),
|
|
4786
7528
|
projectId,
|
|
4787
7529
|
locationName: loc.locationName,
|
|
4788
7530
|
contentHash: lodgingHash,
|
|
@@ -4796,7 +7538,7 @@ async function executeGbpSync(db, runId, projectId, opts) {
|
|
|
4796
7538
|
}
|
|
4797
7539
|
if (attributesChanged) {
|
|
4798
7540
|
tx.insert(gbpAttributesSnapshots).values({
|
|
4799
|
-
id:
|
|
7541
|
+
id: crypto9.randomUUID(),
|
|
4800
7542
|
projectId,
|
|
4801
7543
|
locationName: loc.locationName,
|
|
4802
7544
|
contentHash: attributesHash,
|
|
@@ -4810,7 +7552,7 @@ async function executeGbpSync(db, runId, projectId, opts) {
|
|
|
4810
7552
|
}
|
|
4811
7553
|
if (placeToWrite) {
|
|
4812
7554
|
tx.insert(gbpPlaceDetails).values({
|
|
4813
|
-
id:
|
|
7555
|
+
id: crypto9.randomUUID(),
|
|
4814
7556
|
projectId,
|
|
4815
7557
|
locationName: loc.locationName,
|
|
4816
7558
|
placeId: placeToWrite.placeId,
|
|
@@ -4897,7 +7639,7 @@ async function refreshSelectedLocationProfiles(db, projectId, accessToken, locat
|
|
|
4897
7639
|
}
|
|
4898
7640
|
|
|
4899
7641
|
// src/ads-sync.ts
|
|
4900
|
-
import
|
|
7642
|
+
import crypto10 from "crypto";
|
|
4901
7643
|
import { eq as eq6 } from "drizzle-orm";
|
|
4902
7644
|
|
|
4903
7645
|
// ../integration-openai-ads/src/constants.ts
|
|
@@ -5388,7 +8130,7 @@ async function getAdGroupInsights(apiKey, adGroupId, opts) {
|
|
|
5388
8130
|
}
|
|
5389
8131
|
|
|
5390
8132
|
// src/ads-config.ts
|
|
5391
|
-
function
|
|
8133
|
+
function ensureConnections10(config) {
|
|
5392
8134
|
if (!config.openaiAds) config.openaiAds = {};
|
|
5393
8135
|
if (!config.openaiAds.connections) config.openaiAds.connections = [];
|
|
5394
8136
|
return config.openaiAds.connections;
|
|
@@ -5397,7 +8139,7 @@ function getOpenAiAdsConnection(config, projectName) {
|
|
|
5397
8139
|
return (config.openaiAds?.connections ?? []).find((c) => c.projectName === projectName);
|
|
5398
8140
|
}
|
|
5399
8141
|
function upsertOpenAiAdsConnection(config, connection) {
|
|
5400
|
-
const connections =
|
|
8142
|
+
const connections = ensureConnections10(config);
|
|
5401
8143
|
const index = connections.findIndex((c) => c.projectName === connection.projectName);
|
|
5402
8144
|
if (index === -1) {
|
|
5403
8145
|
connections.push(connection);
|
|
@@ -5625,7 +8367,7 @@ async function executeAdsSync(db, runId, projectId, opts) {
|
|
|
5625
8367
|
}
|
|
5626
8368
|
for (const upsert of insightUpserts) {
|
|
5627
8369
|
tx.insert(adsInsightsDaily).values({
|
|
5628
|
-
id:
|
|
8370
|
+
id: crypto10.randomUUID(),
|
|
5629
8371
|
projectId,
|
|
5630
8372
|
level: upsert.level,
|
|
5631
8373
|
entityId: upsert.entityId,
|
|
@@ -5692,7 +8434,7 @@ async function executeAdsSync(db, runId, projectId, opts) {
|
|
|
5692
8434
|
}
|
|
5693
8435
|
|
|
5694
8436
|
// src/gsc-inspect-sitemap.ts
|
|
5695
|
-
import
|
|
8437
|
+
import crypto12 from "crypto";
|
|
5696
8438
|
import { eq as eq7 } from "drizzle-orm";
|
|
5697
8439
|
|
|
5698
8440
|
// src/sitemap-parser.ts
|
|
@@ -5792,7 +8534,7 @@ async function parseSitemapRecursive(url, urls, visited, depth, isChild) {
|
|
|
5792
8534
|
}
|
|
5793
8535
|
|
|
5794
8536
|
// src/inspect-rate-gate.ts
|
|
5795
|
-
import
|
|
8537
|
+
import crypto11 from "crypto";
|
|
5796
8538
|
var gates = /* @__PURE__ */ new Map();
|
|
5797
8539
|
function sharedRateGate(key) {
|
|
5798
8540
|
let entry = gates.get(key);
|
|
@@ -5811,7 +8553,7 @@ function sharedRateGate(key) {
|
|
|
5811
8553
|
};
|
|
5812
8554
|
}
|
|
5813
8555
|
function credentialGateKey(prefix, secret) {
|
|
5814
|
-
const digest =
|
|
8556
|
+
const digest = crypto11.createHash("sha256").update(secret).digest("hex").slice(0, 12);
|
|
5815
8557
|
return `${prefix}:${digest}`;
|
|
5816
8558
|
}
|
|
5817
8559
|
|
|
@@ -5966,7 +8708,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
5966
8708
|
const rich = ir.richResultsResult;
|
|
5967
8709
|
const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5968
8710
|
db.insert(gscUrlInspections).values({
|
|
5969
|
-
id:
|
|
8711
|
+
id: crypto12.randomUUID(),
|
|
5970
8712
|
projectId,
|
|
5971
8713
|
syncRunId: runId,
|
|
5972
8714
|
url: pageUrl,
|
|
@@ -6024,7 +8766,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
6024
8766
|
}
|
|
6025
8767
|
|
|
6026
8768
|
// src/bing-inspect-sitemap.ts
|
|
6027
|
-
import
|
|
8769
|
+
import crypto13 from "crypto";
|
|
6028
8770
|
import { eq as eq8, desc as desc3 } from "drizzle-orm";
|
|
6029
8771
|
var log7 = createLogger("BingInspectSitemap");
|
|
6030
8772
|
var BING_INSPECT_SPACING_MS = 2e3;
|
|
@@ -6129,7 +8871,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
6129
8871
|
derivedInIndex = false;
|
|
6130
8872
|
}
|
|
6131
8873
|
db.insert(bingUrlInspections).values({
|
|
6132
|
-
id:
|
|
8874
|
+
id: crypto13.randomUUID(),
|
|
6133
8875
|
projectId,
|
|
6134
8876
|
url: pageUrl,
|
|
6135
8877
|
httpCode,
|
|
@@ -6213,7 +8955,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
6213
8955
|
const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
6214
8956
|
const snapNow = (/* @__PURE__ */ new Date()).toISOString();
|
|
6215
8957
|
db.insert(bingCoverageSnapshots).values({
|
|
6216
|
-
id:
|
|
8958
|
+
id: crypto13.randomUUID(),
|
|
6217
8959
|
projectId,
|
|
6218
8960
|
syncRunId: runId,
|
|
6219
8961
|
date: snapshotDate,
|
|
@@ -6254,7 +8996,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
6254
8996
|
}
|
|
6255
8997
|
|
|
6256
8998
|
// src/coverage-refresh.ts
|
|
6257
|
-
import
|
|
8999
|
+
import crypto14 from "crypto";
|
|
6258
9000
|
import { and as and6, desc as desc4, eq as eq9, inArray as inArray3 } from "drizzle-orm";
|
|
6259
9001
|
var log8 = createLogger("CoverageRefresh");
|
|
6260
9002
|
var COVERAGE_REFRESH_MIN_INTERVAL_MS = 60 * 60 * 1e3;
|
|
@@ -6304,7 +9046,7 @@ async function maybeRefreshGscCoverage(db, config, projectId, deps = defaultDeps
|
|
|
6304
9046
|
return null;
|
|
6305
9047
|
}
|
|
6306
9048
|
}
|
|
6307
|
-
const runId =
|
|
9049
|
+
const runId = crypto14.randomUUID();
|
|
6308
9050
|
db.insert(runs).values({
|
|
6309
9051
|
id: runId,
|
|
6310
9052
|
projectId,
|
|
@@ -6327,7 +9069,7 @@ async function maybeRefreshGscCoverage(db, config, projectId, deps = defaultDeps
|
|
|
6327
9069
|
}
|
|
6328
9070
|
|
|
6329
9071
|
// src/commoncrawl-sync.ts
|
|
6330
|
-
import
|
|
9072
|
+
import crypto15 from "crypto";
|
|
6331
9073
|
import path4 from "path";
|
|
6332
9074
|
import { and as and7, eq as eq10, sql as sql4 } from "drizzle-orm";
|
|
6333
9075
|
var log9 = createLogger("CommonCrawlSync");
|
|
@@ -6402,7 +9144,7 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
6402
9144
|
if (!projectIds) continue;
|
|
6403
9145
|
for (const projectId of projectIds) {
|
|
6404
9146
|
expanded.push({
|
|
6405
|
-
id:
|
|
9147
|
+
id: crypto15.randomUUID(),
|
|
6406
9148
|
projectId,
|
|
6407
9149
|
releaseSyncId: syncId,
|
|
6408
9150
|
release,
|
|
@@ -6422,7 +9164,7 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
6422
9164
|
const projectRows = rowsByProject.get(p.id) ?? [];
|
|
6423
9165
|
const summary = computeSummary(projectRows);
|
|
6424
9166
|
tx.insert(backlinkSummaries).values({
|
|
6425
|
-
id:
|
|
9167
|
+
id: crypto15.randomUUID(),
|
|
6426
9168
|
projectId: p.id,
|
|
6427
9169
|
releaseSyncId: syncId,
|
|
6428
9170
|
source: BacklinkSources.commoncrawl,
|
|
@@ -6519,7 +9261,7 @@ function computeSummary(rows) {
|
|
|
6519
9261
|
}
|
|
6520
9262
|
|
|
6521
9263
|
// src/backlink-extract.ts
|
|
6522
|
-
import
|
|
9264
|
+
import crypto16 from "crypto";
|
|
6523
9265
|
import fs3 from "fs";
|
|
6524
9266
|
import { and as and8, desc as desc5, eq as eq11 } from "drizzle-orm";
|
|
6525
9267
|
var log10 = createLogger("BacklinkExtract");
|
|
@@ -6575,7 +9317,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
|
6575
9317
|
).run();
|
|
6576
9318
|
if (rows.length > 0) {
|
|
6577
9319
|
const values = rows.map((r) => ({
|
|
6578
|
-
id:
|
|
9320
|
+
id: crypto16.randomUUID(),
|
|
6579
9321
|
projectId,
|
|
6580
9322
|
releaseSyncId: syncId,
|
|
6581
9323
|
source: BacklinkSources.commoncrawl,
|
|
@@ -6589,7 +9331,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
|
6589
9331
|
}
|
|
6590
9332
|
const summary = computeSummary2(rows);
|
|
6591
9333
|
tx.insert(backlinkSummaries).values({
|
|
6592
|
-
id:
|
|
9334
|
+
id: crypto16.randomUUID(),
|
|
6593
9335
|
projectId,
|
|
6594
9336
|
releaseSyncId: syncId,
|
|
6595
9337
|
source: BacklinkSources.commoncrawl,
|
|
@@ -6632,7 +9374,7 @@ function computeSummary2(rows) {
|
|
|
6632
9374
|
}
|
|
6633
9375
|
|
|
6634
9376
|
// src/discovery-run.ts
|
|
6635
|
-
import
|
|
9377
|
+
import crypto17 from "crypto";
|
|
6636
9378
|
import { and as and9, eq as eq12 } from "drizzle-orm";
|
|
6637
9379
|
var log11 = createLogger("DiscoveryRun");
|
|
6638
9380
|
var DEFAULT_SEED_COUNT = 30;
|
|
@@ -6987,7 +9729,7 @@ function writeDiscoveryInsight(db, input) {
|
|
|
6987
9729
|
eq12(insights.dismissed, false)
|
|
6988
9730
|
)).run();
|
|
6989
9731
|
tx.insert(insights).values({
|
|
6990
|
-
id:
|
|
9732
|
+
id: crypto17.randomUUID(),
|
|
6991
9733
|
projectId: input.projectId,
|
|
6992
9734
|
runId: input.runId,
|
|
6993
9735
|
type: "discovery.basket-divergence",
|
|
@@ -7023,7 +9765,7 @@ function buildDiscoveryInsightTitle(input) {
|
|
|
7023
9765
|
}
|
|
7024
9766
|
|
|
7025
9767
|
// src/execute-site-audit.ts
|
|
7026
|
-
import
|
|
9768
|
+
import crypto19 from "crypto";
|
|
7027
9769
|
import { and as and12, eq as eq15, sql as sql6 } from "drizzle-orm";
|
|
7028
9770
|
import { runSiteCrawl } from "@canonry/aeo-audit";
|
|
7029
9771
|
|
|
@@ -7212,7 +9954,7 @@ async function requestPinnedSiteAuditRoot(target, options) {
|
|
|
7212
9954
|
}
|
|
7213
9955
|
|
|
7214
9956
|
// src/site-crawl-graph-layout.ts
|
|
7215
|
-
import
|
|
9957
|
+
import crypto18 from "crypto";
|
|
7216
9958
|
import { createRequire as createRequire3 } from "module";
|
|
7217
9959
|
import { Worker } from "worker_threads";
|
|
7218
9960
|
import { and as and10, asc, count, desc as desc6, eq as eq13, isNull, ne as ne2, or, sql as sql5 } from "drizzle-orm";
|
|
@@ -7289,7 +10031,7 @@ function noverlapMaxIterations(nodeCount) {
|
|
|
7289
10031
|
}
|
|
7290
10032
|
var COMPONENT_PACKING_SPACING_FACTOR = 3;
|
|
7291
10033
|
function siteCrawlGraphLayoutSettingsFingerprint(settings) {
|
|
7292
|
-
return
|
|
10034
|
+
return crypto18.createHash("sha256").update(JSON.stringify(settings)).digest("hex").slice(0, 8);
|
|
7293
10035
|
}
|
|
7294
10036
|
var SITE_CRAWL_GRAPH_LAYOUT_ALGORITHM = "site-health-fa2-v5";
|
|
7295
10037
|
var SITE_CRAWL_GRAPH_LAYOUT_VERSION = `${SITE_CRAWL_GRAPH_LAYOUT_ALGORITHM}-${siteCrawlGraphLayoutSettingsFingerprint({
|
|
@@ -7786,7 +10528,7 @@ function persistSiteCrawlGraphLayout(db, scope, layout, now = (/* @__PURE__ */ n
|
|
|
7786
10528
|
eq13(siteCrawlGraphLayouts.attemptId, scope.attemptId)
|
|
7787
10529
|
)).run();
|
|
7788
10530
|
tx.insert(siteCrawlGraphLayouts).values({
|
|
7789
|
-
id:
|
|
10531
|
+
id: crypto18.randomUUID(),
|
|
7790
10532
|
projectId: scope.projectId,
|
|
7791
10533
|
runId: scope.runId,
|
|
7792
10534
|
attemptId: scope.attemptId,
|
|
@@ -7806,7 +10548,7 @@ function persistSiteCrawlGraphLayout(db, scope, layout, now = (/* @__PURE__ */ n
|
|
|
7806
10548
|
const batchSize = 250;
|
|
7807
10549
|
for (let offset = 0; offset < layout.nodes.length; offset += batchSize) {
|
|
7808
10550
|
tx.insert(siteCrawlGraphNodes).values(layout.nodes.slice(offset, offset + batchSize).map((node) => ({
|
|
7809
|
-
id:
|
|
10551
|
+
id: crypto18.randomUUID(),
|
|
7810
10552
|
projectId: scope.projectId,
|
|
7811
10553
|
runId: scope.runId,
|
|
7812
10554
|
attemptId: scope.attemptId,
|
|
@@ -7819,7 +10561,7 @@ function persistSiteCrawlGraphLayout(db, scope, layout, now = (/* @__PURE__ */ n
|
|
|
7819
10561
|
}
|
|
7820
10562
|
for (let offset = 0; offset < layout.edges.length; offset += batchSize) {
|
|
7821
10563
|
tx.insert(siteCrawlGraphEdges).values(layout.edges.slice(offset, offset + batchSize).map((edge) => ({
|
|
7822
|
-
id:
|
|
10564
|
+
id: crypto18.randomUUID(),
|
|
7823
10565
|
projectId: scope.projectId,
|
|
7824
10566
|
runId: scope.runId,
|
|
7825
10567
|
attemptId: scope.attemptId,
|
|
@@ -8108,7 +10850,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
|
8108
10850
|
const claim = db.update(runs).set({ status: "running", startedAt }).where(and12(eq15(runs.id, runId), eq15(runs.projectId, projectId), eq15(runs.status, "queued"))).run();
|
|
8109
10851
|
if (claim.changes === 0) return;
|
|
8110
10852
|
const attemptNumber = (db.select({ value: sql6`coalesce(max(${siteCrawlAttempts.attemptNumber}), 0)` }).from(siteCrawlAttempts).where(eq15(siteCrawlAttempts.runId, runId)).get()?.value ?? 0) + 1;
|
|
8111
|
-
const attemptId =
|
|
10853
|
+
const attemptId = crypto19.randomUUID();
|
|
8112
10854
|
db.insert(siteCrawlAttempts).values({
|
|
8113
10855
|
id: attemptId,
|
|
8114
10856
|
projectId,
|
|
@@ -8186,7 +10928,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
|
8186
10928
|
nodeKey: page.key
|
|
8187
10929
|
});
|
|
8188
10930
|
tx.insert(siteCrawlPages).values({
|
|
8189
|
-
id:
|
|
10931
|
+
id: crypto19.randomUUID(),
|
|
8190
10932
|
projectId,
|
|
8191
10933
|
runId,
|
|
8192
10934
|
attemptId,
|
|
@@ -8277,7 +11019,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
|
8277
11019
|
if (isSelfLink(edge.from, edge.to)) return;
|
|
8278
11020
|
observedEdges.set(edge.key, edge);
|
|
8279
11021
|
tx.insert(siteCrawlEdges).values({
|
|
8280
|
-
id:
|
|
11022
|
+
id: crypto19.randomUUID(),
|
|
8281
11023
|
projectId,
|
|
8282
11024
|
runId,
|
|
8283
11025
|
attemptId,
|
|
@@ -8347,7 +11089,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
|
8347
11089
|
return;
|
|
8348
11090
|
}
|
|
8349
11091
|
tx.insert(siteCrawlEventReceipts).values({
|
|
8350
|
-
id:
|
|
11092
|
+
id: crypto19.randomUUID(),
|
|
8351
11093
|
projectId,
|
|
8352
11094
|
runId,
|
|
8353
11095
|
attemptId,
|
|
@@ -8481,7 +11223,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
|
8481
11223
|
if (claim2.changes === 0) return false;
|
|
8482
11224
|
if (hasAuditedPages) {
|
|
8483
11225
|
tx.insert(siteAuditSnapshots).values({
|
|
8484
|
-
id:
|
|
11226
|
+
id: crypto19.randomUUID(),
|
|
8485
11227
|
projectId,
|
|
8486
11228
|
runId,
|
|
8487
11229
|
sitemapUrl: opts.sitemapUrl ?? new URL("/sitemap.xml", crawlRootUrl).href,
|
|
@@ -8499,7 +11241,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
|
8499
11241
|
for (const page of observedPages.values()) {
|
|
8500
11242
|
if (!isLegacyScorecardPage(page)) continue;
|
|
8501
11243
|
tx.insert(siteAuditPages).values({
|
|
8502
|
-
id:
|
|
11244
|
+
id: crypto19.randomUUID(),
|
|
8503
11245
|
projectId,
|
|
8504
11246
|
runId,
|
|
8505
11247
|
url: page.finalUrl ?? page.requestedUrl,
|
|
@@ -8512,7 +11254,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
|
8512
11254
|
}
|
|
8513
11255
|
}
|
|
8514
11256
|
tx.insert(siteCrawlSnapshots).values({
|
|
8515
|
-
id:
|
|
11257
|
+
id: crypto19.randomUUID(),
|
|
8516
11258
|
projectId,
|
|
8517
11259
|
runId,
|
|
8518
11260
|
attemptId,
|
|
@@ -8556,7 +11298,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
|
8556
11298
|
if (opts.checkDeadLinks) {
|
|
8557
11299
|
for (const finding of deadLinkFindings) {
|
|
8558
11300
|
tx.insert(siteCrawlFindings).values({
|
|
8559
|
-
id:
|
|
11301
|
+
id: crypto19.randomUUID(),
|
|
8560
11302
|
projectId,
|
|
8561
11303
|
runId,
|
|
8562
11304
|
attemptId,
|
|
@@ -9087,7 +11829,7 @@ function readStoredGroundingSources(rawResponse) {
|
|
|
9087
11829
|
return result;
|
|
9088
11830
|
}
|
|
9089
11831
|
async function backfillInsightsCommand(project, opts) {
|
|
9090
|
-
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-
|
|
11832
|
+
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-KT6K4ODY.js");
|
|
9091
11833
|
const config = loadConfig();
|
|
9092
11834
|
const db = createClient(config.database);
|
|
9093
11835
|
migrate(db);
|
|
@@ -9663,7 +12405,7 @@ var ProviderRegistry = class {
|
|
|
9663
12405
|
};
|
|
9664
12406
|
|
|
9665
12407
|
// src/scheduler.ts
|
|
9666
|
-
import
|
|
12408
|
+
import crypto20 from "crypto";
|
|
9667
12409
|
import cron from "node-cron";
|
|
9668
12410
|
import { and as and14, eq as eq17, inArray as inArray6, notExists, sql as sql8 } from "drizzle-orm";
|
|
9669
12411
|
var log13 = createLogger("Scheduler");
|
|
@@ -9706,7 +12448,7 @@ var Scheduler = class {
|
|
|
9706
12448
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9707
12449
|
for (const project of projectsWithoutHealth) {
|
|
9708
12450
|
this.db.insert(schedules).values({
|
|
9709
|
-
id:
|
|
12451
|
+
id: crypto20.randomUUID(),
|
|
9710
12452
|
projectId: project.id,
|
|
9711
12453
|
kind: SchedulableRunKinds.doctor,
|
|
9712
12454
|
cronExpr: DEFAULT_HEALTH_CRON,
|
|
@@ -9865,7 +12607,7 @@ var Scheduler = class {
|
|
|
9865
12607
|
log13.warn("gbp-sync.no-callback", { scheduleId, projectId, msg: "host did not register onGbpSyncRequested" });
|
|
9866
12608
|
return;
|
|
9867
12609
|
}
|
|
9868
|
-
const runId2 =
|
|
12610
|
+
const runId2 = crypto20.randomUUID();
|
|
9869
12611
|
this.db.insert(runs).values({
|
|
9870
12612
|
id: runId2,
|
|
9871
12613
|
projectId,
|
|
@@ -9898,7 +12640,7 @@ var Scheduler = class {
|
|
|
9898
12640
|
this.db.update(schedules).set({ nextRunAt, updatedAt: now }).where(eq17(schedules.id, currentSchedule.id)).run();
|
|
9899
12641
|
return;
|
|
9900
12642
|
}
|
|
9901
|
-
const runId2 =
|
|
12643
|
+
const runId2 = crypto20.randomUUID();
|
|
9902
12644
|
this.db.insert(runs).values({
|
|
9903
12645
|
id: runId2,
|
|
9904
12646
|
projectId,
|
|
@@ -9973,7 +12715,7 @@ var Scheduler = class {
|
|
|
9973
12715
|
this.db.update(schedules).set({ nextRunAt, updatedAt: now }).where(eq17(schedules.id, currentSchedule.id)).run();
|
|
9974
12716
|
return;
|
|
9975
12717
|
}
|
|
9976
|
-
const runId2 =
|
|
12718
|
+
const runId2 = crypto20.randomUUID();
|
|
9977
12719
|
const effectiveRequest = normalizeSiteAuditRunRequest({});
|
|
9978
12720
|
this.db.transaction((tx) => {
|
|
9979
12721
|
tx.insert(runs).values({
|
|
@@ -10054,7 +12796,11 @@ async function refreshAllIntegrations(client, projectName) {
|
|
|
10054
12796
|
{ name: "bing", run: () => client.bingInspectSitemap(projectName, {}) },
|
|
10055
12797
|
{ name: "ga", run: () => client.gaSync(projectName, { days: 30 }) },
|
|
10056
12798
|
{ name: "gbp", run: () => client.triggerGbpSync(projectName, {}) },
|
|
10057
|
-
|
|
12799
|
+
// `ads` remains the OpenAI / ChatGPT Ads integration. For scheduled
|
|
12800
|
+
// refreshes, Google providers have explicit names and fan out only here.
|
|
12801
|
+
{ name: "ads", run: () => client.triggerAdsSync(projectName) },
|
|
12802
|
+
{ name: "google-ads", run: () => client.triggerGoogleAdsSync(projectName) },
|
|
12803
|
+
{ name: "gtm", run: () => client.triggerGtmSync(projectName) }
|
|
10058
12804
|
];
|
|
10059
12805
|
const results = await Promise.allSettled(integrations.map((i) => i.run()));
|
|
10060
12806
|
results.forEach((result, idx) => {
|
|
@@ -10071,7 +12817,7 @@ async function refreshAllIntegrations(client, projectName) {
|
|
|
10071
12817
|
|
|
10072
12818
|
// src/notifier.ts
|
|
10073
12819
|
import { eq as eq18, desc as desc7, and as and15, inArray as inArray7, or as or2 } from "drizzle-orm";
|
|
10074
|
-
import
|
|
12820
|
+
import crypto21 from "crypto";
|
|
10075
12821
|
var log15 = createLogger("Notifier");
|
|
10076
12822
|
var Notifier = class {
|
|
10077
12823
|
db;
|
|
@@ -10397,7 +13143,7 @@ var Notifier = class {
|
|
|
10397
13143
|
}
|
|
10398
13144
|
logDelivery(projectId, notificationId, event, status, error) {
|
|
10399
13145
|
this.db.insert(auditLog).values({
|
|
10400
|
-
id:
|
|
13146
|
+
id: crypto21.randomUUID(),
|
|
10401
13147
|
projectId,
|
|
10402
13148
|
actor: "scheduler",
|
|
10403
13149
|
action: `notification.${status}`,
|
|
@@ -10559,7 +13305,7 @@ function analysisResultFromInsights(insights2) {
|
|
|
10559
13305
|
}
|
|
10560
13306
|
|
|
10561
13307
|
// src/agent/session-registry.ts
|
|
10562
|
-
import
|
|
13308
|
+
import crypto25 from "crypto";
|
|
10563
13309
|
import { eq as eq21 } from "drizzle-orm";
|
|
10564
13310
|
|
|
10565
13311
|
// src/agent/session.ts
|
|
@@ -10993,12 +13739,12 @@ function stripProjectFromJsonSchema(jsonSchema) {
|
|
|
10993
13739
|
return { schema: jsonSchema, hadProject: false };
|
|
10994
13740
|
}
|
|
10995
13741
|
const { project: _project, ...remainingProps } = properties;
|
|
10996
|
-
const
|
|
13742
|
+
const required2 = Array.isArray(obj.required) ? obj.required.filter((name) => name !== "project") : obj.required;
|
|
10997
13743
|
const stripped = { ...obj, properties: remainingProps };
|
|
10998
|
-
if (
|
|
13744
|
+
if (required2 === void 0) {
|
|
10999
13745
|
delete stripped.required;
|
|
11000
13746
|
} else {
|
|
11001
|
-
stripped.required =
|
|
13747
|
+
stripped.required = required2;
|
|
11002
13748
|
}
|
|
11003
13749
|
return { schema: stripped, hadProject: true };
|
|
11004
13750
|
}
|
|
@@ -11303,7 +14049,7 @@ function buildAeroStateTools(ctx, opts = {}) {
|
|
|
11303
14049
|
}
|
|
11304
14050
|
|
|
11305
14051
|
// src/agent/llm-usage.ts
|
|
11306
|
-
import
|
|
14052
|
+
import crypto22 from "crypto";
|
|
11307
14053
|
var AeroLlmUsageFeatures = {
|
|
11308
14054
|
turn: "aero.turn"
|
|
11309
14055
|
};
|
|
@@ -11322,7 +14068,7 @@ function recordLlmUsageEvent(args) {
|
|
|
11322
14068
|
const usage = args.message.usage;
|
|
11323
14069
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11324
14070
|
args.db.insert(llmUsageEvents).values({
|
|
11325
|
-
id:
|
|
14071
|
+
id: crypto22.randomUUID(),
|
|
11326
14072
|
projectId: args.projectId,
|
|
11327
14073
|
runId: args.runId,
|
|
11328
14074
|
agentSessionId: args.agentSessionId,
|
|
@@ -11398,7 +14144,7 @@ function splitAeroAnthropicSystemCachePayload(payload, model) {
|
|
|
11398
14144
|
}
|
|
11399
14145
|
|
|
11400
14146
|
// src/agent/tool-usage.ts
|
|
11401
|
-
import
|
|
14147
|
+
import crypto23 from "crypto";
|
|
11402
14148
|
var AeroToolEventStatuses = {
|
|
11403
14149
|
success: "success",
|
|
11404
14150
|
error: "error"
|
|
@@ -11461,7 +14207,7 @@ function createAeroToolUsageHooks(args) {
|
|
|
11461
14207
|
function recordAgentToolEvent(args) {
|
|
11462
14208
|
try {
|
|
11463
14209
|
args.db.insert(agentToolEvents).values({
|
|
11464
|
-
id:
|
|
14210
|
+
id: crypto23.randomUUID(),
|
|
11465
14211
|
projectId: args.projectId,
|
|
11466
14212
|
agentSessionId: args.agentSessionId,
|
|
11467
14213
|
toolCallId: args.toolCallId,
|
|
@@ -11688,7 +14434,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
|
|
|
11688
14434
|
}
|
|
11689
14435
|
|
|
11690
14436
|
// src/agent/memory-store.ts
|
|
11691
|
-
import
|
|
14437
|
+
import crypto24 from "crypto";
|
|
11692
14438
|
import { and as and16, desc as desc8, eq as eq20, like, sql as sql9 } from "drizzle-orm";
|
|
11693
14439
|
var COMPACTION_KEY_PREFIX = "compaction:";
|
|
11694
14440
|
var COMPACTION_NOTES_PER_SESSION = 3;
|
|
@@ -11717,7 +14463,7 @@ function upsertMemoryEntry(db, args) {
|
|
|
11717
14463
|
throw new Error(`memory key prefix "${COMPACTION_KEY_PREFIX}" is reserved for compaction notes`);
|
|
11718
14464
|
}
|
|
11719
14465
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11720
|
-
const id =
|
|
14466
|
+
const id = crypto24.randomUUID();
|
|
11721
14467
|
db.insert(agentMemory).values({
|
|
11722
14468
|
id,
|
|
11723
14469
|
projectId: args.projectId,
|
|
@@ -11754,7 +14500,7 @@ function writeCompactionNote(db, args) {
|
|
|
11754
14500
|
}
|
|
11755
14501
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11756
14502
|
const key = `${COMPACTION_KEY_PREFIX}${args.sessionId}:${now}`;
|
|
11757
|
-
const id =
|
|
14503
|
+
const id = crypto24.randomUUID();
|
|
11758
14504
|
let inserted;
|
|
11759
14505
|
db.transaction((tx) => {
|
|
11760
14506
|
tx.insert(agentMemory).values({
|
|
@@ -12033,7 +14779,7 @@ var SessionRegistry = class {
|
|
|
12033
14779
|
}
|
|
12034
14780
|
const { provider, modelId } = resolveSessionProviderAndModel(this.opts.config, preferences);
|
|
12035
14781
|
const systemPrompt = loadAeroSystemPrompt();
|
|
12036
|
-
const sessionId =
|
|
14782
|
+
const sessionId = crypto25.randomUUID();
|
|
12037
14783
|
const agent = createAeroSession({
|
|
12038
14784
|
projectName,
|
|
12039
14785
|
client: this.opts.client,
|
|
@@ -12412,7 +15158,7 @@ ${lines.join("\n")}
|
|
|
12412
15158
|
insertRow(params) {
|
|
12413
15159
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
12414
15160
|
this.opts.db.insert(agentSessions).values({
|
|
12415
|
-
id: params.id ??
|
|
15161
|
+
id: params.id ?? crypto25.randomUUID(),
|
|
12416
15162
|
projectId: params.projectId,
|
|
12417
15163
|
systemPrompt: params.systemPrompt,
|
|
12418
15164
|
modelProvider: params.provider ?? params.modelProvider ?? AgentProviderIds.claude,
|
|
@@ -12953,7 +15699,7 @@ var SnapshotService = class {
|
|
|
12953
15699
|
});
|
|
12954
15700
|
const enrichedResults = applyBatchAssessment(queryResults, batchAssessment);
|
|
12955
15701
|
const summary = buildSnapshotSummary(companyName, profile.queries, providers, enrichedResults, audit, batchAssessment);
|
|
12956
|
-
const reportCompetitors =
|
|
15702
|
+
const reportCompetitors = uniqueStrings2([
|
|
12957
15703
|
...manualCompetitors,
|
|
12958
15704
|
...summary.topCompetitors.map((entry) => entry.name)
|
|
12959
15705
|
]);
|
|
@@ -13005,8 +15751,8 @@ var SnapshotService = class {
|
|
|
13005
15751
|
return {
|
|
13006
15752
|
industry: parsed.industry?.trim() || "Unknown",
|
|
13007
15753
|
summary: parsed.summary?.trim() || ctx.audit.summary,
|
|
13008
|
-
services:
|
|
13009
|
-
categoryTerms:
|
|
15754
|
+
services: uniqueStrings2(parsed.services ?? []).slice(0, 6),
|
|
15755
|
+
categoryTerms: uniqueStrings2(parsed.categoryTerms ?? []).slice(0, 8),
|
|
13010
15756
|
queries: parsedQueries
|
|
13011
15757
|
};
|
|
13012
15758
|
} catch (err) {
|
|
@@ -13075,9 +15821,9 @@ var SnapshotService = class {
|
|
|
13075
15821
|
accuracyNotes: null,
|
|
13076
15822
|
incorrectClaims: [],
|
|
13077
15823
|
recommendedCompetitors: preliminaryCompetitors,
|
|
13078
|
-
citedDomains:
|
|
15824
|
+
citedDomains: uniqueStrings2(normalized.citedDomains),
|
|
13079
15825
|
groundingSources: normalized.groundingSources,
|
|
13080
|
-
searchQueries:
|
|
15826
|
+
searchQueries: uniqueStrings2(normalized.searchQueries),
|
|
13081
15827
|
answerText: normalized.answerText,
|
|
13082
15828
|
error: null
|
|
13083
15829
|
};
|
|
@@ -13143,14 +15889,14 @@ var SnapshotService = class {
|
|
|
13143
15889
|
mentioned: assessment.mentioned,
|
|
13144
15890
|
describedAccurately: assessment.describedAccurately,
|
|
13145
15891
|
accuracyNotes: assessment.accuracyNotes ?? null,
|
|
13146
|
-
incorrectClaims:
|
|
15892
|
+
incorrectClaims: uniqueStrings2(assessment.incorrectClaims ?? []).slice(0, 5),
|
|
13147
15893
|
...hasReviewedCompetitors ? {
|
|
13148
|
-
recommendedCompetitors:
|
|
15894
|
+
recommendedCompetitors: uniqueStrings2(assessment.recommendedCompetitors ?? []).slice(0, 10)
|
|
13149
15895
|
} : {}
|
|
13150
15896
|
};
|
|
13151
15897
|
}),
|
|
13152
|
-
whatThisMeans:
|
|
13153
|
-
recommendedActions:
|
|
15898
|
+
whatThisMeans: uniqueStrings2(parsed.whatThisMeans ?? []).slice(0, 4),
|
|
15899
|
+
recommendedActions: uniqueStrings2(parsed.recommendedActions ?? []).slice(0, 4)
|
|
13154
15900
|
};
|
|
13155
15901
|
} catch (err) {
|
|
13156
15902
|
log19.warn("response.analysis-failed", {
|
|
@@ -13245,13 +15991,13 @@ function applyBatchAssessment(queryResults, batchAssessment) {
|
|
|
13245
15991
|
};
|
|
13246
15992
|
}
|
|
13247
15993
|
const reviewedCompetitors = assessment.recommendedCompetitors;
|
|
13248
|
-
const recommendedCompetitors = reviewedCompetitors !== void 0 ?
|
|
15994
|
+
const recommendedCompetitors = reviewedCompetitors !== void 0 ? uniqueStrings2(reviewedCompetitors) : result.recommendedCompetitors;
|
|
13249
15995
|
return {
|
|
13250
15996
|
...result,
|
|
13251
15997
|
mentioned: result.mentioned || assessment.mentioned === true,
|
|
13252
15998
|
describedAccurately: assessment.describedAccurately ?? (result.mentioned ? "unknown" : "not-mentioned"),
|
|
13253
15999
|
accuracyNotes: assessment.accuracyNotes ?? result.accuracyNotes ?? null,
|
|
13254
|
-
incorrectClaims:
|
|
16000
|
+
incorrectClaims: uniqueStrings2([
|
|
13255
16001
|
...result.incorrectClaims,
|
|
13256
16002
|
...assessment.incorrectClaims ?? []
|
|
13257
16003
|
]),
|
|
@@ -13281,7 +16027,7 @@ function buildSnapshotSummary(companyName, queries2, providers, queryResults, au
|
|
|
13281
16027
|
const whatThisMeans = batchAssessment.whatThisMeans.length > 0 ? batchAssessment.whatThisMeans : [
|
|
13282
16028
|
defaultMeaning
|
|
13283
16029
|
];
|
|
13284
|
-
const combinedWhatThisMeans =
|
|
16030
|
+
const combinedWhatThisMeans = uniqueStrings2([...whatThisMeans, ...failureNote]).slice(0, 5);
|
|
13285
16031
|
const recommendedActions = batchAssessment.recommendedActions.length > 0 ? batchAssessment.recommendedActions : buildFallbackRecommendedActions(audit);
|
|
13286
16032
|
return {
|
|
13287
16033
|
totalQueries: queries2.length,
|
|
@@ -13324,7 +16070,7 @@ function buildFallbackRecommendedActions(audit) {
|
|
|
13324
16070
|
"Add machine-readable trust signals such as schema, FAQs, and llms.txt support.",
|
|
13325
16071
|
"Build comparison and proof content that makes the category fit unmistakable."
|
|
13326
16072
|
];
|
|
13327
|
-
return
|
|
16073
|
+
return uniqueStrings2([...weakestFactors, ...defaults]).slice(0, 4);
|
|
13328
16074
|
}
|
|
13329
16075
|
function citesTargetDomain(citedDomains, groundingSources, targetDomain) {
|
|
13330
16076
|
for (const domain of citedDomains) {
|
|
@@ -13389,11 +16135,11 @@ function parseJsonObject(input) {
|
|
|
13389
16135
|
}
|
|
13390
16136
|
function normalizeStringList(values) {
|
|
13391
16137
|
const items = values.flatMap((value) => value.split(","));
|
|
13392
|
-
return
|
|
16138
|
+
return uniqueStrings2(
|
|
13393
16139
|
items.map((value) => value.trim()).filter(Boolean)
|
|
13394
16140
|
);
|
|
13395
16141
|
}
|
|
13396
|
-
function
|
|
16142
|
+
function uniqueStrings2(values) {
|
|
13397
16143
|
if (!Array.isArray(values)) return [];
|
|
13398
16144
|
return [...new Set(
|
|
13399
16145
|
values.filter((value) => typeof value === "string").map((value) => value.trim()).filter(Boolean)
|
|
@@ -13528,6 +16274,253 @@ function finalizeResearchRun(db, runId, fatalError) {
|
|
|
13528
16274
|
}).where(eq23(researchRuns.id, runId)).run();
|
|
13529
16275
|
}
|
|
13530
16276
|
|
|
16277
|
+
// src/google-marketing-sync.ts
|
|
16278
|
+
import { and as and18, eq as eq24, isNull as isNull3 } from "drizzle-orm";
|
|
16279
|
+
function safeErrorMessage(error) {
|
|
16280
|
+
const raw = describeError(error);
|
|
16281
|
+
return raw.replace(/(["']?authorization["']?\s*[:=]\s*["']?bearer\s+)[^\s,"'}]+/gi, "$1[redacted]").replace(/(["']?(?:developer|access|refresh|client)[_-]?(?:token|secret)["']?\s*[:=]\s*["']?)[^\s,"'}]+/gi, "$1[redacted]").replace(/[\r\n\t]+/g, " ").slice(0, 1e3);
|
|
16282
|
+
}
|
|
16283
|
+
function googleAdsSnapshotValues(snapshot) {
|
|
16284
|
+
return {
|
|
16285
|
+
...snapshot.metadata,
|
|
16286
|
+
payload: snapshot.payload
|
|
16287
|
+
};
|
|
16288
|
+
}
|
|
16289
|
+
function gtmSnapshotValues(snapshot) {
|
|
16290
|
+
return {
|
|
16291
|
+
...snapshot.metadata,
|
|
16292
|
+
payload: snapshot.payload
|
|
16293
|
+
};
|
|
16294
|
+
}
|
|
16295
|
+
function assertSnapshotContext(metadata, expected) {
|
|
16296
|
+
if (metadata.projectId !== expected.projectId || metadata.connectionId !== expected.connectionId || metadata.runId !== expected.runId) {
|
|
16297
|
+
throw new Error("Google Marketing runtime returned snapshot metadata for a different run context.");
|
|
16298
|
+
}
|
|
16299
|
+
}
|
|
16300
|
+
function requireSyncRun(db, runId, projectId, kind) {
|
|
16301
|
+
const run = db.select().from(runs).where(and18(
|
|
16302
|
+
eq24(runs.id, runId),
|
|
16303
|
+
eq24(runs.projectId, projectId),
|
|
16304
|
+
eq24(runs.kind, kind)
|
|
16305
|
+
)).get();
|
|
16306
|
+
if (!run) throw new Error(`Run ${runId} is not a ${kind} run for this project.`);
|
|
16307
|
+
if (run.status !== RunStatuses.queued) {
|
|
16308
|
+
throw new Error(`Run ${runId} is ${run.status}; only queued Google Marketing runs can start.`);
|
|
16309
|
+
}
|
|
16310
|
+
return run;
|
|
16311
|
+
}
|
|
16312
|
+
function markFailed(db, runId, projectId, kind, expectedStatus, error) {
|
|
16313
|
+
db.update(runs).set({
|
|
16314
|
+
status: RunStatuses.failed,
|
|
16315
|
+
error: serializeRunError({ message: safeErrorMessage(error) }),
|
|
16316
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16317
|
+
}).where(and18(
|
|
16318
|
+
eq24(runs.id, runId),
|
|
16319
|
+
eq24(runs.projectId, projectId),
|
|
16320
|
+
eq24(runs.kind, kind),
|
|
16321
|
+
eq24(runs.status, expectedStatus)
|
|
16322
|
+
)).run();
|
|
16323
|
+
}
|
|
16324
|
+
function claimSyncRun(db, runId, projectId, kind, startedAt) {
|
|
16325
|
+
const claim = db.update(runs).set({ status: RunStatuses.running, startedAt, error: null }).where(and18(
|
|
16326
|
+
eq24(runs.id, runId),
|
|
16327
|
+
eq24(runs.projectId, projectId),
|
|
16328
|
+
eq24(runs.kind, kind),
|
|
16329
|
+
eq24(runs.status, RunStatuses.queued)
|
|
16330
|
+
)).run();
|
|
16331
|
+
return claim.changes === 1;
|
|
16332
|
+
}
|
|
16333
|
+
async function executeGoogleAdsMarketingSync(db, runtime, runId, projectId) {
|
|
16334
|
+
let claimed = false;
|
|
16335
|
+
try {
|
|
16336
|
+
requireSyncRun(db, runId, projectId, RunKinds["google-ads-sync"]);
|
|
16337
|
+
const project = db.select().from(projects).where(eq24(projects.id, projectId)).get();
|
|
16338
|
+
if (!project) throw new Error(`Project ${projectId} was not found.`);
|
|
16339
|
+
const connection = db.select().from(googleAdsConnections).where(eq24(googleAdsConnections.projectId, projectId)).get();
|
|
16340
|
+
if (!connection?.selectedCustomerId) {
|
|
16341
|
+
throw new Error("Google Ads needs a selected customer before it can sync.");
|
|
16342
|
+
}
|
|
16343
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
16344
|
+
if (!claimSyncRun(db, runId, projectId, RunKinds["google-ads-sync"], startedAt)) {
|
|
16345
|
+
throw new Error(`Run ${runId} could not be claimed for Google Ads sync.`);
|
|
16346
|
+
}
|
|
16347
|
+
claimed = true;
|
|
16348
|
+
const result = await runtime.syncGoogleAds({
|
|
16349
|
+
project: { id: project.id, name: project.name },
|
|
16350
|
+
connectionId: connection.id,
|
|
16351
|
+
runId,
|
|
16352
|
+
selection: {
|
|
16353
|
+
customerId: connection.selectedCustomerId,
|
|
16354
|
+
loginCustomerId: connection.selectedLoginCustomerId,
|
|
16355
|
+
selectedAt: connection.lastValidatedAt
|
|
16356
|
+
}
|
|
16357
|
+
});
|
|
16358
|
+
const expected = { projectId, connectionId: connection.id, runId };
|
|
16359
|
+
assertSnapshotContext(result.accessibleCustomers.metadata, expected);
|
|
16360
|
+
assertSnapshotContext(result.inventory.metadata, expected);
|
|
16361
|
+
if (result.metrics) assertSnapshotContext(result.metrics.metadata, expected);
|
|
16362
|
+
if (result.accessibleCustomers.payload.kind !== GoogleAdsSnapshotKinds["accessible-customers"]) {
|
|
16363
|
+
throw new Error("Google Marketing runtime returned the wrong customer snapshot kind.");
|
|
16364
|
+
}
|
|
16365
|
+
if (result.inventory.payload.kind !== GoogleAdsSnapshotKinds.inventory) {
|
|
16366
|
+
throw new Error("Google Marketing runtime returned the wrong inventory snapshot kind.");
|
|
16367
|
+
}
|
|
16368
|
+
if (result.metrics && result.metrics.payload.kind !== GoogleAdsSnapshotKinds["campaign-metrics"]) {
|
|
16369
|
+
throw new Error("Google Marketing runtime returned the wrong metrics snapshot kind.");
|
|
16370
|
+
}
|
|
16371
|
+
if (result.accessibleCustomers.metadata.customerId !== connection.selectedCustomerId || result.inventory.metadata.customerId !== connection.selectedCustomerId || result.inventory.payload.data.customerId !== connection.selectedCustomerId || result.metrics && result.metrics.metadata.customerId !== connection.selectedCustomerId) {
|
|
16372
|
+
throw new Error("Google Ads returned evidence for a different customer.");
|
|
16373
|
+
}
|
|
16374
|
+
const observedSelection = result.accessibleCustomers.payload.data.selection;
|
|
16375
|
+
if (observedSelection.customerId !== connection.selectedCustomerId || observedSelection.loginCustomerId !== connection.selectedLoginCustomerId) {
|
|
16376
|
+
throw new Error("Google Ads returned evidence for a different customer selection.");
|
|
16377
|
+
}
|
|
16378
|
+
const selectedCustomer = result.accessibleCustomers.payload.data.customers.find(
|
|
16379
|
+
(customer) => customer.customerId === connection.selectedCustomerId
|
|
16380
|
+
);
|
|
16381
|
+
if (!selectedCustomer) {
|
|
16382
|
+
throw new Error("Google Ads did not return the selected customer in its validated snapshot.");
|
|
16383
|
+
}
|
|
16384
|
+
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
16385
|
+
db.transaction((tx) => {
|
|
16386
|
+
const terminalClaim = tx.update(runs).set({
|
|
16387
|
+
status: RunStatuses.completed,
|
|
16388
|
+
finishedAt,
|
|
16389
|
+
error: null
|
|
16390
|
+
}).where(and18(
|
|
16391
|
+
eq24(runs.id, runId),
|
|
16392
|
+
eq24(runs.projectId, projectId),
|
|
16393
|
+
eq24(runs.kind, RunKinds["google-ads-sync"]),
|
|
16394
|
+
eq24(runs.status, RunStatuses.running)
|
|
16395
|
+
)).run();
|
|
16396
|
+
if (terminalClaim.changes !== 1) {
|
|
16397
|
+
throw new Error(`Run ${runId} was no longer running when Google Ads evidence was ready.`);
|
|
16398
|
+
}
|
|
16399
|
+
const connectionClaim = tx.update(googleAdsConnections).set({
|
|
16400
|
+
selectedCustomerName: selectedCustomer.descriptiveName,
|
|
16401
|
+
selectedCustomerCurrencyCode: selectedCustomer.currencyCode,
|
|
16402
|
+
selectedCustomerTimeZone: selectedCustomer.timeZone,
|
|
16403
|
+
selectedCustomerStatus: selectedCustomer.status,
|
|
16404
|
+
lastValidatedAt: result.accessibleCustomers.metadata.capturedAt,
|
|
16405
|
+
lastCustomerSnapshotId: result.accessibleCustomers.metadata.id,
|
|
16406
|
+
lastInventorySnapshotAt: result.inventory.metadata.capturedAt,
|
|
16407
|
+
lastInventorySnapshotId: result.inventory.metadata.id,
|
|
16408
|
+
lastMetricsSnapshotAt: result.metrics?.metadata.capturedAt ?? connection.lastMetricsSnapshotAt,
|
|
16409
|
+
lastMetricsSnapshotId: result.metrics?.metadata.id ?? connection.lastMetricsSnapshotId,
|
|
16410
|
+
updatedAt: finishedAt
|
|
16411
|
+
}).where(and18(
|
|
16412
|
+
eq24(googleAdsConnections.id, connection.id),
|
|
16413
|
+
eq24(googleAdsConnections.projectId, projectId),
|
|
16414
|
+
eq24(googleAdsConnections.selectedCustomerId, connection.selectedCustomerId),
|
|
16415
|
+
connection.selectedLoginCustomerId === null ? isNull3(googleAdsConnections.selectedLoginCustomerId) : eq24(googleAdsConnections.selectedLoginCustomerId, connection.selectedLoginCustomerId),
|
|
16416
|
+
eq24(googleAdsConnections.selectionGeneration, connection.selectionGeneration)
|
|
16417
|
+
)).run();
|
|
16418
|
+
if (connectionClaim.changes !== 1) {
|
|
16419
|
+
throw new Error("Google Ads selection changed while its evidence was being collected.");
|
|
16420
|
+
}
|
|
16421
|
+
tx.insert(googleAdsRawSnapshots).values([
|
|
16422
|
+
googleAdsSnapshotValues(result.accessibleCustomers),
|
|
16423
|
+
googleAdsSnapshotValues(result.inventory),
|
|
16424
|
+
...result.metrics ? [googleAdsSnapshotValues(result.metrics)] : []
|
|
16425
|
+
]).run();
|
|
16426
|
+
});
|
|
16427
|
+
} catch (error) {
|
|
16428
|
+
markFailed(
|
|
16429
|
+
db,
|
|
16430
|
+
runId,
|
|
16431
|
+
projectId,
|
|
16432
|
+
RunKinds["google-ads-sync"],
|
|
16433
|
+
claimed ? RunStatuses.running : RunStatuses.queued,
|
|
16434
|
+
error
|
|
16435
|
+
);
|
|
16436
|
+
throw error;
|
|
16437
|
+
}
|
|
16438
|
+
}
|
|
16439
|
+
async function executeGtmMarketingSync(db, runtime, runId, projectId) {
|
|
16440
|
+
let claimed = false;
|
|
16441
|
+
try {
|
|
16442
|
+
requireSyncRun(db, runId, projectId, RunKinds["gtm-sync"]);
|
|
16443
|
+
const project = db.select().from(projects).where(eq24(projects.id, projectId)).get();
|
|
16444
|
+
if (!project) throw new Error(`Project ${projectId} was not found.`);
|
|
16445
|
+
const connection = db.select().from(gtmConnections).where(eq24(gtmConnections.projectId, projectId)).get();
|
|
16446
|
+
if (!connection?.selectedAccountId || !connection.selectedContainerId) {
|
|
16447
|
+
throw new Error("GTM needs a selected account and container before it can sync.");
|
|
16448
|
+
}
|
|
16449
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
16450
|
+
if (!claimSyncRun(db, runId, projectId, RunKinds["gtm-sync"], startedAt)) {
|
|
16451
|
+
throw new Error(`Run ${runId} could not be claimed for GTM sync.`);
|
|
16452
|
+
}
|
|
16453
|
+
claimed = true;
|
|
16454
|
+
const snapshot = await runtime.syncGtm({
|
|
16455
|
+
project: { id: project.id, name: project.name },
|
|
16456
|
+
connectionId: connection.id,
|
|
16457
|
+
runId,
|
|
16458
|
+
selection: {
|
|
16459
|
+
accountId: connection.selectedAccountId,
|
|
16460
|
+
containerId: connection.selectedContainerId,
|
|
16461
|
+
workspaceId: connection.selectedWorkspaceId
|
|
16462
|
+
},
|
|
16463
|
+
expectedHostname: hostOf(project.canonicalDomain) ?? project.canonicalDomain
|
|
16464
|
+
});
|
|
16465
|
+
assertSnapshotContext(snapshot.metadata, { projectId, connectionId: connection.id, runId });
|
|
16466
|
+
if (snapshot.payload.kind !== "container") {
|
|
16467
|
+
throw new Error("Google Marketing runtime returned the wrong GTM snapshot kind.");
|
|
16468
|
+
}
|
|
16469
|
+
const graph = snapshot.payload.data;
|
|
16470
|
+
if (snapshot.metadata.accountId !== connection.selectedAccountId || snapshot.metadata.containerId !== connection.selectedContainerId || snapshot.metadata.workspaceId !== connection.selectedWorkspaceId) {
|
|
16471
|
+
throw new Error("GTM returned evidence for a different account, container, or workspace.");
|
|
16472
|
+
}
|
|
16473
|
+
const selectedWorkspace = connection.selectedWorkspaceId ? graph.workspaces.find((workspace) => workspace.id === connection.selectedWorkspaceId) : null;
|
|
16474
|
+
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
16475
|
+
db.transaction((tx) => {
|
|
16476
|
+
const terminalClaim = tx.update(runs).set({
|
|
16477
|
+
status: RunStatuses.completed,
|
|
16478
|
+
finishedAt,
|
|
16479
|
+
error: null
|
|
16480
|
+
}).where(and18(
|
|
16481
|
+
eq24(runs.id, runId),
|
|
16482
|
+
eq24(runs.projectId, projectId),
|
|
16483
|
+
eq24(runs.kind, RunKinds["gtm-sync"]),
|
|
16484
|
+
eq24(runs.status, RunStatuses.running)
|
|
16485
|
+
)).run();
|
|
16486
|
+
if (terminalClaim.changes !== 1) {
|
|
16487
|
+
throw new Error(`Run ${runId} was no longer running when GTM evidence was ready.`);
|
|
16488
|
+
}
|
|
16489
|
+
const connectionClaim = tx.update(gtmConnections).set({
|
|
16490
|
+
selectedAccountName: graph.account.name,
|
|
16491
|
+
selectedContainerName: graph.container.name,
|
|
16492
|
+
selectedContainerPublicId: graph.container.publicId,
|
|
16493
|
+
selectedWorkspaceName: selectedWorkspace?.name ?? null,
|
|
16494
|
+
lastValidatedAt: snapshot.metadata.capturedAt,
|
|
16495
|
+
lastSnapshotAt: snapshot.metadata.capturedAt,
|
|
16496
|
+
lastSnapshotId: snapshot.metadata.id,
|
|
16497
|
+
updatedAt: finishedAt
|
|
16498
|
+
}).where(and18(
|
|
16499
|
+
eq24(gtmConnections.id, connection.id),
|
|
16500
|
+
eq24(gtmConnections.projectId, projectId),
|
|
16501
|
+
eq24(gtmConnections.selectedAccountId, connection.selectedAccountId),
|
|
16502
|
+
eq24(gtmConnections.selectedContainerId, connection.selectedContainerId),
|
|
16503
|
+
connection.selectedWorkspaceId === null ? isNull3(gtmConnections.selectedWorkspaceId) : eq24(gtmConnections.selectedWorkspaceId, connection.selectedWorkspaceId),
|
|
16504
|
+
eq24(gtmConnections.selectionGeneration, connection.selectionGeneration)
|
|
16505
|
+
)).run();
|
|
16506
|
+
if (connectionClaim.changes !== 1) {
|
|
16507
|
+
throw new Error("GTM selection changed while its evidence was being collected.");
|
|
16508
|
+
}
|
|
16509
|
+
tx.insert(gtmRawSnapshots).values(gtmSnapshotValues(snapshot)).run();
|
|
16510
|
+
});
|
|
16511
|
+
} catch (error) {
|
|
16512
|
+
markFailed(
|
|
16513
|
+
db,
|
|
16514
|
+
runId,
|
|
16515
|
+
projectId,
|
|
16516
|
+
RunKinds["gtm-sync"],
|
|
16517
|
+
claimed ? RunStatuses.running : RunStatuses.queued,
|
|
16518
|
+
error
|
|
16519
|
+
);
|
|
16520
|
+
throw error;
|
|
16521
|
+
}
|
|
16522
|
+
}
|
|
16523
|
+
|
|
13531
16524
|
// src/server.ts
|
|
13532
16525
|
var _require3 = createRequire4(import.meta.url);
|
|
13533
16526
|
var { version: PKG_VERSION2 } = _require3("../package.json");
|
|
@@ -13595,14 +16588,14 @@ function summarizeProviderConfig(config) {
|
|
|
13595
16588
|
};
|
|
13596
16589
|
}
|
|
13597
16590
|
function hashApiKey(key) {
|
|
13598
|
-
return
|
|
16591
|
+
return crypto26.createHash("sha256").update(key).digest("hex");
|
|
13599
16592
|
}
|
|
13600
16593
|
var DASHBOARD_SCRYPT_KEYLEN = 64;
|
|
13601
16594
|
var DASHBOARD_SCRYPT_COST = 1 << 15;
|
|
13602
16595
|
var DASHBOARD_SCRYPT_MAXMEM = 64 * 1024 * 1024;
|
|
13603
16596
|
function hashDashboardPassword(password) {
|
|
13604
|
-
const salt =
|
|
13605
|
-
const derived =
|
|
16597
|
+
const salt = crypto26.randomBytes(16);
|
|
16598
|
+
const derived = crypto26.scryptSync(password, salt, DASHBOARD_SCRYPT_KEYLEN, {
|
|
13606
16599
|
N: DASHBOARD_SCRYPT_COST,
|
|
13607
16600
|
maxmem: DASHBOARD_SCRYPT_MAXMEM
|
|
13608
16601
|
});
|
|
@@ -13623,14 +16616,14 @@ function verifyDashboardPassword(password, storedHash) {
|
|
|
13623
16616
|
} catch {
|
|
13624
16617
|
return { ok: false, needsRehash: false };
|
|
13625
16618
|
}
|
|
13626
|
-
const derived =
|
|
16619
|
+
const derived = crypto26.scryptSync(password, salt, expected.length, {
|
|
13627
16620
|
N: DASHBOARD_SCRYPT_COST,
|
|
13628
16621
|
maxmem: DASHBOARD_SCRYPT_MAXMEM
|
|
13629
16622
|
});
|
|
13630
16623
|
if (derived.length !== expected.length)
|
|
13631
16624
|
return { ok: false, needsRehash: false };
|
|
13632
16625
|
return {
|
|
13633
|
-
ok:
|
|
16626
|
+
ok: crypto26.timingSafeEqual(derived, expected),
|
|
13634
16627
|
needsRehash: false
|
|
13635
16628
|
};
|
|
13636
16629
|
}
|
|
@@ -13639,7 +16632,7 @@ function verifyDashboardPassword(password, storedHash) {
|
|
|
13639
16632
|
const expected = Buffer.from(storedHash, "hex");
|
|
13640
16633
|
if (candidate.length !== expected.length)
|
|
13641
16634
|
return { ok: false, needsRehash: false };
|
|
13642
|
-
const ok =
|
|
16635
|
+
const ok = crypto26.timingSafeEqual(candidate, expected);
|
|
13643
16636
|
return { ok, needsRehash: ok };
|
|
13644
16637
|
}
|
|
13645
16638
|
return { ok: false, needsRehash: false };
|
|
@@ -13748,6 +16741,126 @@ function resolveGooglePublicUrl(config, basePath) {
|
|
|
13748
16741
|
return void 0;
|
|
13749
16742
|
}
|
|
13750
16743
|
}
|
|
16744
|
+
function cloneGoogleAdsConfig(config) {
|
|
16745
|
+
if (!config) return void 0;
|
|
16746
|
+
return {
|
|
16747
|
+
...config,
|
|
16748
|
+
...config.connections === void 0 ? {} : {
|
|
16749
|
+
connections: config.connections.map((connection) => ({
|
|
16750
|
+
...connection,
|
|
16751
|
+
...connection.scopes === void 0 ? {} : { scopes: [...connection.scopes] }
|
|
16752
|
+
}))
|
|
16753
|
+
}
|
|
16754
|
+
};
|
|
16755
|
+
}
|
|
16756
|
+
function cloneGtmConfig(config) {
|
|
16757
|
+
if (!config) return void 0;
|
|
16758
|
+
return {
|
|
16759
|
+
...config,
|
|
16760
|
+
...config.connections === void 0 ? {} : {
|
|
16761
|
+
connections: config.connections.map((connection) => ({
|
|
16762
|
+
...connection,
|
|
16763
|
+
...connection.scopes === void 0 ? {} : { scopes: [...connection.scopes] }
|
|
16764
|
+
}))
|
|
16765
|
+
}
|
|
16766
|
+
};
|
|
16767
|
+
}
|
|
16768
|
+
function createGoogleMarketingConfigCredentialStore(input) {
|
|
16769
|
+
const persistConfigPatch = input.saveConfigPatch ?? saveConfigPatch;
|
|
16770
|
+
const env = input.env ?? process.env;
|
|
16771
|
+
const randomUUID = input.randomUUID ?? crypto26.randomUUID;
|
|
16772
|
+
const snapshot = () => ({
|
|
16773
|
+
googleAds: cloneGoogleAdsConfig(input.config.googleAds),
|
|
16774
|
+
gtm: cloneGtmConfig(input.config.gtm)
|
|
16775
|
+
});
|
|
16776
|
+
const restore = (previous) => {
|
|
16777
|
+
input.config.googleAds = previous.googleAds;
|
|
16778
|
+
input.config.gtm = previous.gtm;
|
|
16779
|
+
};
|
|
16780
|
+
return {
|
|
16781
|
+
hasGoogleAdsDeveloperToken: () => Boolean(
|
|
16782
|
+
env.GOOGLE_ADS_DEVELOPER_TOKEN?.trim() || getGoogleAdsAuthConfig(input.config).developerToken?.trim()
|
|
16783
|
+
),
|
|
16784
|
+
get: (project, provider) => {
|
|
16785
|
+
if (provider === GoogleMarketingProviders["google-ads"]) {
|
|
16786
|
+
const connection2 = getGoogleAdsConnection(input.config, project.id);
|
|
16787
|
+
if (!connection2) return void 0;
|
|
16788
|
+
return {
|
|
16789
|
+
accessToken: connection2.accessToken ?? null,
|
|
16790
|
+
refreshToken: connection2.refreshToken ?? null,
|
|
16791
|
+
expiresAt: connection2.tokenExpiresAt ?? null,
|
|
16792
|
+
scopes: connection2.scopes ?? [],
|
|
16793
|
+
developerToken: getGoogleAdsAuthConfig(input.config).developerToken ?? null,
|
|
16794
|
+
createdAt: connection2.createdAt,
|
|
16795
|
+
updatedAt: connection2.updatedAt
|
|
16796
|
+
};
|
|
16797
|
+
}
|
|
16798
|
+
const connection = getGtmConnection(input.config, project.id);
|
|
16799
|
+
if (!connection) return void 0;
|
|
16800
|
+
return {
|
|
16801
|
+
accessToken: connection.accessToken ?? null,
|
|
16802
|
+
refreshToken: connection.refreshToken ?? null,
|
|
16803
|
+
expiresAt: connection.tokenExpiresAt ?? null,
|
|
16804
|
+
scopes: connection.scopes ?? [],
|
|
16805
|
+
createdAt: connection.createdAt,
|
|
16806
|
+
updatedAt: connection.updatedAt
|
|
16807
|
+
};
|
|
16808
|
+
},
|
|
16809
|
+
upsert: (project, provider, credential) => {
|
|
16810
|
+
const previous = snapshot();
|
|
16811
|
+
try {
|
|
16812
|
+
if (provider === GoogleMarketingProviders["google-ads"]) {
|
|
16813
|
+
if (credential.developerToken) {
|
|
16814
|
+
setGoogleAdsAuthConfig(input.config, { developerToken: credential.developerToken });
|
|
16815
|
+
}
|
|
16816
|
+
upsertGoogleAdsConnection(input.config, {
|
|
16817
|
+
projectId: project.id,
|
|
16818
|
+
projectName: project.name,
|
|
16819
|
+
credentialGeneration: randomUUID(),
|
|
16820
|
+
...credential.accessToken ? { accessToken: credential.accessToken } : {},
|
|
16821
|
+
refreshToken: credential.refreshToken ?? null,
|
|
16822
|
+
tokenExpiresAt: credential.expiresAt ?? null,
|
|
16823
|
+
scopes: credential.scopes,
|
|
16824
|
+
createdAt: credential.createdAt,
|
|
16825
|
+
updatedAt: credential.updatedAt
|
|
16826
|
+
});
|
|
16827
|
+
} else {
|
|
16828
|
+
upsertGtmConnection(input.config, {
|
|
16829
|
+
projectId: project.id,
|
|
16830
|
+
projectName: project.name,
|
|
16831
|
+
credentialGeneration: randomUUID(),
|
|
16832
|
+
...credential.accessToken ? { accessToken: credential.accessToken } : {},
|
|
16833
|
+
refreshToken: credential.refreshToken ?? null,
|
|
16834
|
+
tokenExpiresAt: credential.expiresAt ?? null,
|
|
16835
|
+
scopes: credential.scopes,
|
|
16836
|
+
createdAt: credential.createdAt,
|
|
16837
|
+
updatedAt: credential.updatedAt
|
|
16838
|
+
});
|
|
16839
|
+
}
|
|
16840
|
+
persistConfigPatch(input.config);
|
|
16841
|
+
} catch (error) {
|
|
16842
|
+
restore(previous);
|
|
16843
|
+
throw error;
|
|
16844
|
+
}
|
|
16845
|
+
return () => {
|
|
16846
|
+
restore(previous);
|
|
16847
|
+
persistConfigPatch(input.config);
|
|
16848
|
+
};
|
|
16849
|
+
},
|
|
16850
|
+
delete: (project, provider) => {
|
|
16851
|
+
const previous = snapshot();
|
|
16852
|
+
const removed = provider === GoogleMarketingProviders["google-ads"] ? removeGoogleAdsConnection(input.config, project.id) : removeGtmConnection(input.config, project.id);
|
|
16853
|
+
if (!removed) return false;
|
|
16854
|
+
try {
|
|
16855
|
+
persistConfigPatch(input.config);
|
|
16856
|
+
} catch (error) {
|
|
16857
|
+
restore(previous);
|
|
16858
|
+
throw error;
|
|
16859
|
+
}
|
|
16860
|
+
return true;
|
|
16861
|
+
}
|
|
16862
|
+
};
|
|
16863
|
+
}
|
|
13751
16864
|
async function createServer(opts) {
|
|
13752
16865
|
const logger = opts.logger === false ? false : process.stdout.isTTY ? {
|
|
13753
16866
|
transport: {
|
|
@@ -13847,7 +16960,7 @@ async function createServer(opts) {
|
|
|
13847
16960
|
(runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
|
|
13848
16961
|
async (ctx) => {
|
|
13849
16962
|
if (!sessionRegistry) return;
|
|
13850
|
-
const project = opts.db.select({ name: projects.name }).from(projects).where(
|
|
16963
|
+
const project = opts.db.select({ name: projects.name }).from(projects).where(eq25(projects.id, ctx.projectId)).get();
|
|
13851
16964
|
if (!project) return;
|
|
13852
16965
|
let content;
|
|
13853
16966
|
if (ctx.kind === RunKinds["aeo-discover-probe"]) {
|
|
@@ -13870,6 +16983,99 @@ async function createServer(opts) {
|
|
|
13870
16983
|
);
|
|
13871
16984
|
jobRunner.onRunCompleted = (runId, projectId) => runCoordinator.onRunCompleted(runId, projectId);
|
|
13872
16985
|
const snapshotService = new SnapshotService(registry);
|
|
16986
|
+
const liveProjectIds = new Set(
|
|
16987
|
+
opts.db.select({ id: projects.id }).from(projects).all().map((project) => project.id)
|
|
16988
|
+
);
|
|
16989
|
+
const removedGoogleMarketingCredentials = removeLegacyGoogleAdsConnections(opts.config) + removeLegacyGtmConnections(opts.config) + removeOrphanedGoogleAdsConnections(opts.config, liveProjectIds) + removeOrphanedGtmConnections(opts.config, liveProjectIds);
|
|
16990
|
+
if (removedGoogleMarketingCredentials > 0) {
|
|
16991
|
+
saveConfigPatch({
|
|
16992
|
+
googleAds: opts.config.googleAds,
|
|
16993
|
+
gtm: opts.config.gtm
|
|
16994
|
+
});
|
|
16995
|
+
}
|
|
16996
|
+
const googleMarketingRuntime = createGoogleMarketingRuntime({
|
|
16997
|
+
config: opts.config,
|
|
16998
|
+
saveConfigPatch
|
|
16999
|
+
});
|
|
17000
|
+
const googleMarketingCredentialStore = createGoogleMarketingConfigCredentialStore({
|
|
17001
|
+
config: opts.config
|
|
17002
|
+
});
|
|
17003
|
+
const prepareGoogleMarketingCredentialDelete = (projectId) => {
|
|
17004
|
+
const previousGoogleAdsConnections = opts.config.googleAds?.connections;
|
|
17005
|
+
const previousGtmConnections = opts.config.gtm?.connections;
|
|
17006
|
+
const restore = () => {
|
|
17007
|
+
if (opts.config.googleAds) opts.config.googleAds.connections = previousGoogleAdsConnections;
|
|
17008
|
+
if (opts.config.gtm) opts.config.gtm.connections = previousGtmConnections;
|
|
17009
|
+
};
|
|
17010
|
+
const removedGoogleAds = removeGoogleAdsConnection(opts.config, projectId);
|
|
17011
|
+
const removedGtm = removeGtmConnection(opts.config, projectId);
|
|
17012
|
+
if (!removedGoogleAds && !removedGtm) return void 0;
|
|
17013
|
+
try {
|
|
17014
|
+
saveConfigPatch({
|
|
17015
|
+
googleAds: opts.config.googleAds,
|
|
17016
|
+
gtm: opts.config.gtm
|
|
17017
|
+
});
|
|
17018
|
+
} catch (error) {
|
|
17019
|
+
restore();
|
|
17020
|
+
throw error;
|
|
17021
|
+
}
|
|
17022
|
+
return () => {
|
|
17023
|
+
restore();
|
|
17024
|
+
try {
|
|
17025
|
+
saveConfigPatch({
|
|
17026
|
+
googleAds: opts.config.googleAds,
|
|
17027
|
+
gtm: opts.config.gtm
|
|
17028
|
+
});
|
|
17029
|
+
} catch (error) {
|
|
17030
|
+
removeGoogleAdsConnection(opts.config, projectId);
|
|
17031
|
+
removeGtmConnection(opts.config, projectId);
|
|
17032
|
+
throw error;
|
|
17033
|
+
}
|
|
17034
|
+
};
|
|
17035
|
+
};
|
|
17036
|
+
const googleMarketingOAuth = {
|
|
17037
|
+
authorizationUrl: (input) => {
|
|
17038
|
+
const auth = input.provider === GoogleMarketingProviders["google-ads"] ? getGoogleAdsAuthConfig(opts.config) : getGtmAuthConfig(opts.config);
|
|
17039
|
+
if (!auth.clientId || !auth.clientSecret) {
|
|
17040
|
+
throw new Error("Google OAuth client credentials are not configured.");
|
|
17041
|
+
}
|
|
17042
|
+
return getAuthUrl(auth.clientId, input.redirectUri, [...input.scopes], input.state);
|
|
17043
|
+
},
|
|
17044
|
+
exchangeCode: async (input) => {
|
|
17045
|
+
const auth = input.provider === GoogleMarketingProviders["google-ads"] ? getGoogleAdsAuthConfig(opts.config) : getGtmAuthConfig(opts.config);
|
|
17046
|
+
if (!auth.clientId || !auth.clientSecret) {
|
|
17047
|
+
throw new Error("Google OAuth client credentials are not configured.");
|
|
17048
|
+
}
|
|
17049
|
+
const tokens = await exchangeCode(
|
|
17050
|
+
auth.clientId,
|
|
17051
|
+
auth.clientSecret,
|
|
17052
|
+
input.code,
|
|
17053
|
+
input.redirectUri
|
|
17054
|
+
);
|
|
17055
|
+
return {
|
|
17056
|
+
accessToken: tokens.access_token,
|
|
17057
|
+
refreshToken: tokens.refresh_token ?? null,
|
|
17058
|
+
expiresAt: Number.isFinite(tokens.expires_in) && tokens.expires_in > 0 ? new Date(Date.now() + tokens.expires_in * 1e3).toISOString() : null,
|
|
17059
|
+
scopes: tokens.scope?.split(/\s+/).filter(Boolean) ?? [input.provider === GoogleMarketingProviders["google-ads"] ? GOOGLE_ADS_OAUTH_SCOPE : GTM_READONLY_SCOPE]
|
|
17060
|
+
};
|
|
17061
|
+
}
|
|
17062
|
+
};
|
|
17063
|
+
const googleMarketingLiveReader = {
|
|
17064
|
+
listGoogleAdsCustomers: (project) => googleMarketingRuntime.listGoogleAdsCustomers(project),
|
|
17065
|
+
listGtmAccounts: (project) => googleMarketingRuntime.listGtmAccounts(project),
|
|
17066
|
+
listGtmContainers: (project, accountId) => googleMarketingRuntime.listGtmContainers(project, accountId),
|
|
17067
|
+
listGtmWorkspaces: (project, accountId, containerId) => googleMarketingRuntime.listGtmWorkspaces(project, accountId, containerId)
|
|
17068
|
+
};
|
|
17069
|
+
const runGoogleAdsMarketingSync = (runId, projectId) => {
|
|
17070
|
+
executeGoogleAdsMarketingSync(opts.db, googleMarketingRuntime, runId, projectId).then(() => runCoordinator.onRunCompleted(runId, projectId)).catch(() => {
|
|
17071
|
+
app.log.error({ runId, projectId }, "Google Ads sync failed");
|
|
17072
|
+
});
|
|
17073
|
+
};
|
|
17074
|
+
const runGtmMarketingSync = (runId, projectId) => {
|
|
17075
|
+
executeGtmMarketingSync(opts.db, googleMarketingRuntime, runId, projectId).then(() => runCoordinator.onRunCompleted(runId, projectId)).catch(() => {
|
|
17076
|
+
app.log.error({ runId, projectId }, "GTM sync failed");
|
|
17077
|
+
});
|
|
17078
|
+
};
|
|
13873
17079
|
const orphanedOpenClawDir = path8.join(os4.homedir(), ".openclaw-aero");
|
|
13874
17080
|
if (fs7.existsSync(orphanedOpenClawDir)) {
|
|
13875
17081
|
app.log.warn(
|
|
@@ -14197,7 +17403,7 @@ async function createServer(opts) {
|
|
|
14197
17403
|
void (async () => {
|
|
14198
17404
|
try {
|
|
14199
17405
|
const report = await aeroClient.runDoctor({ project: projectName });
|
|
14200
|
-
const project = opts.db.select().from(projects).where(
|
|
17406
|
+
const project = opts.db.select().from(projects).where(eq25(projects.name, projectName)).get();
|
|
14201
17407
|
if (!project) {
|
|
14202
17408
|
app.log.warn({ projectName }, "doctor schedule fired for an unknown project");
|
|
14203
17409
|
return;
|
|
@@ -14225,9 +17431,9 @@ async function createServer(opts) {
|
|
|
14225
17431
|
});
|
|
14226
17432
|
if (!probed) return;
|
|
14227
17433
|
const alreadySynced = opts.db.select().from(ccReleaseSyncs).where(
|
|
14228
|
-
|
|
14229
|
-
|
|
14230
|
-
|
|
17434
|
+
and19(
|
|
17435
|
+
eq25(ccReleaseSyncs.release, probed.release),
|
|
17436
|
+
eq25(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)
|
|
14231
17437
|
)
|
|
14232
17438
|
).limit(1).get();
|
|
14233
17439
|
if (alreadySynced) {
|
|
@@ -14408,7 +17614,7 @@ async function createServer(opts) {
|
|
|
14408
17614
|
return removed;
|
|
14409
17615
|
}
|
|
14410
17616
|
};
|
|
14411
|
-
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ??
|
|
17617
|
+
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto26.randomBytes(32).toString("hex");
|
|
14412
17618
|
const googleConnectionStore = {
|
|
14413
17619
|
listConnections: (domain) => listGoogleConnections(opts.config, domain),
|
|
14414
17620
|
getConnection: (domain, connectionType) => getGoogleConnection(opts.config, domain, connectionType),
|
|
@@ -14484,11 +17690,11 @@ async function createServer(opts) {
|
|
|
14484
17690
|
const googlePublicUrl = resolveGooglePublicUrl(opts.config, basePath);
|
|
14485
17691
|
if (opts.config.apiKey) {
|
|
14486
17692
|
const keyHash = hashApiKey(opts.config.apiKey);
|
|
14487
|
-
const existing = opts.db.select().from(apiKeys).where(
|
|
17693
|
+
const existing = opts.db.select().from(apiKeys).where(eq25(apiKeys.keyHash, keyHash)).get();
|
|
14488
17694
|
if (!existing) {
|
|
14489
17695
|
const prefix = opts.config.apiKey.slice(0, 12);
|
|
14490
17696
|
opts.db.insert(apiKeys).values({
|
|
14491
|
-
id: `key_${
|
|
17697
|
+
id: `key_${crypto26.randomBytes(8).toString("hex")}`,
|
|
14492
17698
|
name: "default",
|
|
14493
17699
|
keyHash,
|
|
14494
17700
|
keyPrefix: prefix,
|
|
@@ -14512,7 +17718,7 @@ async function createServer(opts) {
|
|
|
14512
17718
|
};
|
|
14513
17719
|
const createSession = (apiKeyId) => {
|
|
14514
17720
|
pruneExpiredSessions();
|
|
14515
|
-
const sessionId =
|
|
17721
|
+
const sessionId = crypto26.randomBytes(32).toString("hex");
|
|
14516
17722
|
sessions.set(sessionId, {
|
|
14517
17723
|
apiKeyId,
|
|
14518
17724
|
expiresAt: Date.now() + SESSION_TTL_MS
|
|
@@ -14536,7 +17742,7 @@ async function createServer(opts) {
|
|
|
14536
17742
|
};
|
|
14537
17743
|
const getDefaultApiKey = () => {
|
|
14538
17744
|
if (!opts.config.apiKey) return void 0;
|
|
14539
|
-
return opts.db.select().from(apiKeys).where(
|
|
17745
|
+
return opts.db.select().from(apiKeys).where(eq25(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
|
|
14540
17746
|
};
|
|
14541
17747
|
const createPasswordSession = (reply) => {
|
|
14542
17748
|
const key = getDefaultApiKey();
|
|
@@ -14560,7 +17766,7 @@ async function createServer(opts) {
|
|
|
14560
17766
|
if (!header) return false;
|
|
14561
17767
|
const parts = header.split(" ");
|
|
14562
17768
|
if (parts.length !== 2 || parts[0] !== "Bearer") return false;
|
|
14563
|
-
const key = opts.db.select().from(apiKeys).where(
|
|
17769
|
+
const key = opts.db.select().from(apiKeys).where(eq25(apiKeys.keyHash, hashApiKey(parts[1]))).get();
|
|
14564
17770
|
return Boolean(key && !key.revokedAt);
|
|
14565
17771
|
};
|
|
14566
17772
|
const namedAccountsInUse = () => anyUsersExist(opts.db);
|
|
@@ -14650,12 +17856,12 @@ async function createServer(opts) {
|
|
|
14650
17856
|
return reply.send({ authenticated: true });
|
|
14651
17857
|
}
|
|
14652
17858
|
if (apiKey) {
|
|
14653
|
-
const key = opts.db.select().from(apiKeys).where(
|
|
17859
|
+
const key = opts.db.select().from(apiKeys).where(eq25(apiKeys.keyHash, hashApiKey(apiKey))).get();
|
|
14654
17860
|
if (!key || key.revokedAt) {
|
|
14655
17861
|
const err2 = authInvalid();
|
|
14656
17862
|
return reply.status(err2.statusCode).send(err2.toJSON());
|
|
14657
17863
|
}
|
|
14658
|
-
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
17864
|
+
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(apiKeys.id, key.id)).run();
|
|
14659
17865
|
const sessionId = createSession(key.id);
|
|
14660
17866
|
reply.header(
|
|
14661
17867
|
"set-cookie",
|
|
@@ -14797,9 +18003,61 @@ async function createServer(opts) {
|
|
|
14797
18003
|
// Resolved fresh each call so a key added at runtime (settings API) shows
|
|
14798
18004
|
// up immediately in the `config.agent-providers` doctor check.
|
|
14799
18005
|
getAgentProviderSummary: () => buildAgentProvidersResponse(opts.config).providers,
|
|
18006
|
+
getGoogleMarketingDoctorInput: (ctx) => {
|
|
18007
|
+
if (!ctx.project) return null;
|
|
18008
|
+
const googleAds = opts.db.select().from(googleAdsConnections).where(eq25(googleAdsConnections.projectId, ctx.project.id)).get();
|
|
18009
|
+
const gtm = opts.db.select().from(gtmConnections).where(eq25(gtmConnections.projectId, ctx.project.id)).get();
|
|
18010
|
+
const googleAdsCredential = getGoogleAdsConnection(opts.config, ctx.project.id);
|
|
18011
|
+
const gtmCredential = getGtmConnection(opts.config, ctx.project.id);
|
|
18012
|
+
const googleAdsConnected = Boolean(googleAdsCredential?.accessToken);
|
|
18013
|
+
const gtmConnected = Boolean(gtmCredential?.accessToken);
|
|
18014
|
+
return {
|
|
18015
|
+
googleAds: googleAds && googleAdsConnected ? {
|
|
18016
|
+
credentialsPresent: Boolean(
|
|
18017
|
+
googleAdsCredential?.accessToken || googleAdsCredential?.refreshToken
|
|
18018
|
+
),
|
|
18019
|
+
grantedScopes: googleAds.scopes,
|
|
18020
|
+
selectedLoginCustomerId: googleAds.selectedLoginCustomerId,
|
|
18021
|
+
selectedCustomerId: googleAds.selectedCustomerId,
|
|
18022
|
+
latestSnapshotAt: [
|
|
18023
|
+
googleAds.lastInventorySnapshotAt,
|
|
18024
|
+
googleAds.lastMetricsSnapshotAt
|
|
18025
|
+
].filter((value) => Boolean(value)).sort().at(-1) ?? null
|
|
18026
|
+
} : null,
|
|
18027
|
+
gtm: gtm && gtmConnected ? {
|
|
18028
|
+
credentialsPresent: Boolean(gtmCredential?.accessToken || gtmCredential?.refreshToken),
|
|
18029
|
+
grantedScopes: gtm.scopes,
|
|
18030
|
+
selectedAccountId: gtm.selectedAccountId,
|
|
18031
|
+
selectedContainerId: gtm.selectedContainerId,
|
|
18032
|
+
selectedWorkspaceId: gtm.selectedWorkspaceId,
|
|
18033
|
+
latestSnapshotAt: gtm.lastSnapshotAt
|
|
18034
|
+
} : null
|
|
18035
|
+
};
|
|
18036
|
+
},
|
|
14800
18037
|
googleConnectionStore,
|
|
14801
18038
|
googleStateSecret,
|
|
14802
18039
|
publicUrl: googlePublicUrl,
|
|
18040
|
+
googleMarketingCredentialStore,
|
|
18041
|
+
googleMarketingOAuth,
|
|
18042
|
+
googleMarketingOAuthScopes: {
|
|
18043
|
+
[GoogleMarketingProviders["google-ads"]]: [GOOGLE_ADS_OAUTH_SCOPE],
|
|
18044
|
+
[GoogleMarketingProviders.gtm]: [GTM_READONLY_SCOPE]
|
|
18045
|
+
},
|
|
18046
|
+
googleMarketingLiveReader,
|
|
18047
|
+
assessConversionTrackingIntegrity: ({ contract, googleAdsSnapshot, gtmSnapshot }) => {
|
|
18048
|
+
const googleAdsInventory = googleAdsSnapshot?.payload.kind === "inventory" ? googleAdsSnapshot.payload.data : null;
|
|
18049
|
+
const gtmLiveGraph = gtmSnapshot?.payload.kind === "container" ? gtmSnapshot.payload.data.live : gtmSnapshot?.payload.kind === "live" ? gtmSnapshot.payload.data : null;
|
|
18050
|
+
return assessConversionTrackingIntegrity({
|
|
18051
|
+
contract,
|
|
18052
|
+
googleAdsInventory,
|
|
18053
|
+
googleAdsEvidenceId: googleAdsSnapshot?.metadata.id,
|
|
18054
|
+
gtmLiveGraph,
|
|
18055
|
+
gtmEvidenceId: gtmSnapshot?.metadata.id,
|
|
18056
|
+
evaluatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
18057
|
+
});
|
|
18058
|
+
},
|
|
18059
|
+
onGoogleAdsSyncRequested: runGoogleAdsMarketingSync,
|
|
18060
|
+
onGtmSyncRequested: runGtmMarketingSync,
|
|
14803
18061
|
onGscSyncRequested: (runId, projectId, syncOpts) => {
|
|
14804
18062
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
14805
18063
|
if (!googleClientId || !googleClientSecret) {
|
|
@@ -14872,7 +18130,7 @@ async function createServer(opts) {
|
|
|
14872
18130
|
deps: {
|
|
14873
18131
|
enqueueAutoExtract: ({ projectId, release: r }) => {
|
|
14874
18132
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14875
|
-
const runId =
|
|
18133
|
+
const runId = crypto26.randomUUID();
|
|
14876
18134
|
opts.db.insert(runs).values({
|
|
14877
18135
|
id: runId,
|
|
14878
18136
|
projectId,
|
|
@@ -14997,7 +18255,7 @@ async function createServer(opts) {
|
|
|
14997
18255
|
...inspectOpts,
|
|
14998
18256
|
config: opts.config
|
|
14999
18257
|
}).then(() => {
|
|
15000
|
-
const finished = opts.db.select({ status: runs.status }).from(runs).where(
|
|
18258
|
+
const finished = opts.db.select({ status: runs.status }).from(runs).where(eq25(runs.id, runId)).get();
|
|
15001
18259
|
if (finished?.status === RunStatuses.completed || finished?.status === RunStatuses.partial) {
|
|
15002
18260
|
return maybeRefreshGscCoverage(
|
|
15003
18261
|
opts.db,
|
|
@@ -15107,7 +18365,7 @@ async function createServer(opts) {
|
|
|
15107
18365
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
15108
18366
|
opts.db.insert(auditLog).values(
|
|
15109
18367
|
targetProjectIds.map((projectId) => ({
|
|
15110
|
-
id:
|
|
18368
|
+
id: crypto26.randomUUID(),
|
|
15111
18369
|
projectId,
|
|
15112
18370
|
actor: "api",
|
|
15113
18371
|
action: existing ? "provider.updated" : "provider.created",
|
|
@@ -15152,8 +18410,17 @@ async function createServer(opts) {
|
|
|
15152
18410
|
if (action === "upsert") scheduler.upsert(projectId, kind);
|
|
15153
18411
|
if (action === "delete") scheduler.remove(projectId, kind);
|
|
15154
18412
|
},
|
|
18413
|
+
onProjectDeleting: prepareGoogleMarketingCredentialDelete,
|
|
15155
18414
|
onProjectDeleted: (projectId) => {
|
|
15156
18415
|
scheduler.removeAllForProject(projectId);
|
|
18416
|
+
const removedGoogleAds = removeGoogleAdsConnection(opts.config, projectId);
|
|
18417
|
+
const removedGtm = removeGtmConnection(opts.config, projectId);
|
|
18418
|
+
if (removedGoogleAds || removedGtm) {
|
|
18419
|
+
saveConfigPatch({
|
|
18420
|
+
googleAds: opts.config.googleAds,
|
|
18421
|
+
gtm: opts.config.gtm
|
|
18422
|
+
});
|
|
18423
|
+
}
|
|
15157
18424
|
},
|
|
15158
18425
|
onAliasesChanged: (projectId, projectName) => {
|
|
15159
18426
|
setImmediate(() => {
|
|
@@ -15378,7 +18645,7 @@ async function createServer(opts) {
|
|
|
15378
18645
|
}
|
|
15379
18646
|
checkLatestVersionForServer();
|
|
15380
18647
|
scheduler.start();
|
|
15381
|
-
for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(
|
|
18648
|
+
for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(eq25(researchRuns.status, ResearchRunStatuses.queued)).all()) {
|
|
15382
18649
|
dispatchResearchRun(run.id, run.projectId);
|
|
15383
18650
|
}
|
|
15384
18651
|
app.addHook("onClose", async () => {
|
|
@@ -15469,5 +18736,8 @@ export {
|
|
|
15469
18736
|
coerceAgentProvider,
|
|
15470
18737
|
AERO_TOOL_PROFILES,
|
|
15471
18738
|
isAeroToolProfile,
|
|
18739
|
+
GoogleMarketingRuntimeError,
|
|
18740
|
+
createGoogleMarketingCredentialStore,
|
|
18741
|
+
createGoogleMarketingRuntime,
|
|
15472
18742
|
createServer
|
|
15473
18743
|
};
|