@rawdash/connector-branch 0.25.0 → 0.27.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/README.md +8 -8
- package/dist/index.d.ts +71 -52
- package/dist/index.js +108 -66
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@rawdash/connector-branch)
|
|
6
6
|
[](https://github.com/rawdash/rawdash/blob/main/LICENSE)
|
|
7
7
|
|
|
8
|
-
Sync Branch install attribution metrics (installs, opens, conversions) and deep-link click events from the
|
|
8
|
+
Sync Branch install attribution metrics (installs, opens, conversions) and deep-link click events from the Query API for mobile attribution dashboards.
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
@@ -15,7 +15,7 @@ npm install @rawdash/connector-branch
|
|
|
15
15
|
|
|
16
16
|
## Authentication
|
|
17
17
|
|
|
18
|
-
A Branch app key and secret,
|
|
18
|
+
A Branch app key and secret, sent together in the Query API request body to authenticate each call.
|
|
19
19
|
|
|
20
20
|
1. In the Branch dashboard, open Account Settings -> Profile and copy the Branch Key (starts with `key_live_`).
|
|
21
21
|
2. On the same screen, reveal and copy the Branch Secret (starts with `secret_live_`). Both values are app-scoped; keep them in a secret store.
|
|
@@ -32,12 +32,12 @@ A Branch app key and secret, used together to authenticate Cross-Platform Analyt
|
|
|
32
32
|
|
|
33
33
|
## Resources
|
|
34
34
|
|
|
35
|
-
- **`branch_install_metrics`** _(metric)_ - Daily Branch attribution metrics bucketed by channel and campaign. Primary value is `installs`; `opens
|
|
35
|
+
- **`branch_install_metrics`** _(metric)_ - Daily Branch attribution metrics bucketed by channel and campaign. Primary value is `installs`; `opens` and `conversions` are carried as attributes.
|
|
36
36
|
- Endpoint: `POST /v1/query/analytics`
|
|
37
37
|
- Unit: installs
|
|
38
38
|
- Granularity: day
|
|
39
|
-
- Dimensions: `date`, `channel`, `campaign`, `installs`, `opens`, `conversions
|
|
40
|
-
- Merges three
|
|
39
|
+
- Dimensions: `date`, `channel`, `campaign`, `installs`, `opens`, `conversions`
|
|
40
|
+
- Merges three Query API calls (data_source=eo_install, eo_open, eo_custom_event) keyed by (date, channel, campaign). Rows with missing channel or campaign are recorded as `null` for that attribute.
|
|
41
41
|
- **`branch_deep_link_event`** _(event)_ - Daily aggregated Branch deep-link click events bucketed by channel, campaign, and feature. One event per (date, channel, campaign, feature) row carrying the daily click count.
|
|
42
42
|
- Endpoint: `POST /v1/query/analytics`
|
|
43
43
|
- Sourced from data_source=eo_click. Event id encodes the bucket so resyncs are idempotent.
|
|
@@ -104,12 +104,12 @@ export default defineConfig({
|
|
|
104
104
|
|
|
105
105
|
## Rate limits
|
|
106
106
|
|
|
107
|
-
Branch
|
|
107
|
+
The Branch Query API allows roughly 5 requests/second, 20/minute, and 150/hour per app. Because each sync splits its window into <=7-day segments and paginates, a wide window fans out to many requests; the connector relies on the shared HTTP client to honor 429 responses and the `Retry-After` header with backoff.
|
|
108
108
|
|
|
109
109
|
## Limitations
|
|
110
110
|
|
|
111
|
-
- Daily granularity only - the connector requests `granularity=day` from the Branch
|
|
112
|
-
-
|
|
111
|
+
- Daily granularity only - the connector requests `granularity=day` from the Branch Query API to keep result cardinality bounded.
|
|
112
|
+
- Branch rejects windows wider than 7 days, so each requested range is split into <=7-day segments and fetched one segment at a time.
|
|
113
113
|
- Deep-link events are aggregated daily click counts per (date, channel, campaign, feature). Individual click-level records require the Branch Daily Export API which is intentionally out of scope.
|
|
114
114
|
|
|
115
115
|
## Links
|
package/dist/index.d.ts
CHANGED
|
@@ -33,34 +33,33 @@ type BranchCredentials = typeof branchCredentials;
|
|
|
33
33
|
declare const PHASE_ORDER: readonly ["install_metrics", "deep_link_events"];
|
|
34
34
|
type BranchPhase = (typeof PHASE_ORDER)[number];
|
|
35
35
|
type BranchResource = BranchPhase;
|
|
36
|
-
declare const INSTALL_DATA_SOURCES: readonly ["eo_install", "eo_open", "
|
|
36
|
+
declare const INSTALL_DATA_SOURCES: readonly ["eo_install", "eo_open", "eo_custom_event"];
|
|
37
37
|
type InstallDataSource = (typeof INSTALL_DATA_SOURCES)[number];
|
|
38
38
|
declare const installResultRowSchema: z.ZodObject<{
|
|
39
|
-
|
|
39
|
+
timestamp: z.ZodString;
|
|
40
40
|
result: z.ZodObject<{
|
|
41
|
-
timestamp: z.ZodString;
|
|
42
41
|
last_attributed_touch_data_tilde_channel: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
43
42
|
last_attributed_touch_data_tilde_campaign: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
44
|
-
|
|
43
|
+
unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
|
|
45
44
|
}, z.core.$strip>;
|
|
46
45
|
}, z.core.$strip>;
|
|
47
46
|
declare const clickResultRowSchema: z.ZodObject<{
|
|
48
|
-
|
|
47
|
+
timestamp: z.ZodString;
|
|
49
48
|
result: z.ZodObject<{
|
|
50
|
-
timestamp: z.ZodString;
|
|
51
49
|
last_attributed_touch_data_tilde_channel: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
52
50
|
last_attributed_touch_data_tilde_campaign: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
53
51
|
last_attributed_touch_data_tilde_feature: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
52
|
+
unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
|
|
54
53
|
}, z.core.$strip>;
|
|
55
54
|
}, z.core.$strip>;
|
|
56
55
|
declare const branchResources: {
|
|
57
56
|
readonly branch_install_metrics: {
|
|
58
57
|
readonly shape: "metric";
|
|
59
|
-
readonly description: "Daily Branch attribution metrics bucketed by channel and campaign. Primary value is `installs`; `opens
|
|
58
|
+
readonly description: "Daily Branch attribution metrics bucketed by channel and campaign. Primary value is `installs`; `opens` and `conversions` are carried as attributes.";
|
|
60
59
|
readonly endpoint: "POST /v1/query/analytics";
|
|
61
60
|
readonly unit: "installs";
|
|
62
61
|
readonly granularity: "day";
|
|
63
|
-
readonly notes: "Merges three
|
|
62
|
+
readonly notes: "Merges three Query API calls (data_source=eo_install, eo_open, eo_custom_event) keyed by (date, channel, campaign). Rows with missing channel or campaign are recorded as `null` for that attribute.";
|
|
64
63
|
readonly dimensions: [{
|
|
65
64
|
readonly name: "date";
|
|
66
65
|
readonly description: "Calendar day of the metric sample (UTC).";
|
|
@@ -78,44 +77,47 @@ declare const branchResources: {
|
|
|
78
77
|
readonly description: "Attributed app opens on the day.";
|
|
79
78
|
}, {
|
|
80
79
|
readonly name: "conversions";
|
|
81
|
-
readonly description: "Attributed in-app
|
|
82
|
-
}, {
|
|
83
|
-
readonly name: "costEstimated";
|
|
84
|
-
readonly description: "Estimated cost in the app local currency (only populated for ad-network-integrated channels; 0 otherwise).";
|
|
80
|
+
readonly description: "Attributed in-app custom-event conversions on the day.";
|
|
85
81
|
}];
|
|
86
82
|
readonly responses: {
|
|
87
83
|
readonly install_metrics_installs: z.ZodObject<{
|
|
88
84
|
results: z.ZodArray<z.ZodObject<{
|
|
89
|
-
|
|
85
|
+
timestamp: z.ZodString;
|
|
90
86
|
result: z.ZodObject<{
|
|
91
|
-
timestamp: z.ZodString;
|
|
92
87
|
last_attributed_touch_data_tilde_channel: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
93
88
|
last_attributed_touch_data_tilde_campaign: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
94
|
-
|
|
89
|
+
unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
|
|
95
90
|
}, z.core.$strip>;
|
|
96
91
|
}, z.core.$strip>>;
|
|
92
|
+
paging: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
93
|
+
next_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
94
|
+
}, z.core.$strip>>>;
|
|
97
95
|
}, z.core.$strip>;
|
|
98
96
|
readonly install_metrics_opens: z.ZodObject<{
|
|
99
97
|
results: z.ZodArray<z.ZodObject<{
|
|
100
|
-
|
|
98
|
+
timestamp: z.ZodString;
|
|
101
99
|
result: z.ZodObject<{
|
|
102
|
-
timestamp: z.ZodString;
|
|
103
100
|
last_attributed_touch_data_tilde_channel: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
104
101
|
last_attributed_touch_data_tilde_campaign: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
105
|
-
|
|
102
|
+
unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
|
|
106
103
|
}, z.core.$strip>;
|
|
107
104
|
}, z.core.$strip>>;
|
|
105
|
+
paging: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
106
|
+
next_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
107
|
+
}, z.core.$strip>>>;
|
|
108
108
|
}, z.core.$strip>;
|
|
109
109
|
readonly install_metrics_conversions: z.ZodObject<{
|
|
110
110
|
results: z.ZodArray<z.ZodObject<{
|
|
111
|
-
|
|
111
|
+
timestamp: z.ZodString;
|
|
112
112
|
result: z.ZodObject<{
|
|
113
|
-
timestamp: z.ZodString;
|
|
114
113
|
last_attributed_touch_data_tilde_channel: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
115
114
|
last_attributed_touch_data_tilde_campaign: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
116
|
-
|
|
115
|
+
unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
|
|
117
116
|
}, z.core.$strip>;
|
|
118
117
|
}, z.core.$strip>>;
|
|
118
|
+
paging: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
119
|
+
next_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
120
|
+
}, z.core.$strip>>>;
|
|
119
121
|
}, z.core.$strip>;
|
|
120
122
|
};
|
|
121
123
|
};
|
|
@@ -144,14 +146,17 @@ declare const branchResources: {
|
|
|
144
146
|
readonly responses: {
|
|
145
147
|
readonly deep_link_events: z.ZodObject<{
|
|
146
148
|
results: z.ZodArray<z.ZodObject<{
|
|
147
|
-
|
|
149
|
+
timestamp: z.ZodString;
|
|
148
150
|
result: z.ZodObject<{
|
|
149
|
-
timestamp: z.ZodString;
|
|
150
151
|
last_attributed_touch_data_tilde_channel: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
151
152
|
last_attributed_touch_data_tilde_campaign: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
152
153
|
last_attributed_touch_data_tilde_feature: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
154
|
+
unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
|
|
153
155
|
}, z.core.$strip>;
|
|
154
156
|
}, z.core.$strip>>;
|
|
157
|
+
paging: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
158
|
+
next_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
159
|
+
}, z.core.$strip>>>;
|
|
155
160
|
}, z.core.$strip>;
|
|
156
161
|
};
|
|
157
162
|
};
|
|
@@ -170,7 +175,6 @@ interface InstallBucket {
|
|
|
170
175
|
installs: number;
|
|
171
176
|
opens: number;
|
|
172
177
|
conversions: number;
|
|
173
|
-
costEstimated: number;
|
|
174
178
|
}
|
|
175
179
|
declare function mergeInstallBuckets(rowsByDataSource: Record<InstallDataSource, BranchInstallResultRow[]>): InstallBucket[];
|
|
176
180
|
declare function installBucketToMetricSample(bucket: InstallBucket): MetricSample;
|
|
@@ -181,11 +185,11 @@ declare class BranchConnector extends BaseConnector<BranchSettings, BranchCreden
|
|
|
181
185
|
static readonly resources: {
|
|
182
186
|
readonly branch_install_metrics: {
|
|
183
187
|
readonly shape: "metric";
|
|
184
|
-
readonly description: "Daily Branch attribution metrics bucketed by channel and campaign. Primary value is `installs`; `opens
|
|
188
|
+
readonly description: "Daily Branch attribution metrics bucketed by channel and campaign. Primary value is `installs`; `opens` and `conversions` are carried as attributes.";
|
|
185
189
|
readonly endpoint: "POST /v1/query/analytics";
|
|
186
190
|
readonly unit: "installs";
|
|
187
191
|
readonly granularity: "day";
|
|
188
|
-
readonly notes: "Merges three
|
|
192
|
+
readonly notes: "Merges three Query API calls (data_source=eo_install, eo_open, eo_custom_event) keyed by (date, channel, campaign). Rows with missing channel or campaign are recorded as `null` for that attribute.";
|
|
189
193
|
readonly dimensions: [{
|
|
190
194
|
readonly name: "date";
|
|
191
195
|
readonly description: "Calendar day of the metric sample (UTC).";
|
|
@@ -203,44 +207,47 @@ declare class BranchConnector extends BaseConnector<BranchSettings, BranchCreden
|
|
|
203
207
|
readonly description: "Attributed app opens on the day.";
|
|
204
208
|
}, {
|
|
205
209
|
readonly name: "conversions";
|
|
206
|
-
readonly description: "Attributed in-app
|
|
207
|
-
}, {
|
|
208
|
-
readonly name: "costEstimated";
|
|
209
|
-
readonly description: "Estimated cost in the app local currency (only populated for ad-network-integrated channels; 0 otherwise).";
|
|
210
|
+
readonly description: "Attributed in-app custom-event conversions on the day.";
|
|
210
211
|
}];
|
|
211
212
|
readonly responses: {
|
|
212
213
|
readonly install_metrics_installs: z.ZodObject<{
|
|
213
214
|
results: z.ZodArray<z.ZodObject<{
|
|
214
|
-
|
|
215
|
+
timestamp: z.ZodString;
|
|
215
216
|
result: z.ZodObject<{
|
|
216
|
-
timestamp: z.ZodString;
|
|
217
217
|
last_attributed_touch_data_tilde_channel: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
218
218
|
last_attributed_touch_data_tilde_campaign: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
219
|
-
|
|
219
|
+
unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
|
|
220
220
|
}, z.core.$strip>;
|
|
221
221
|
}, z.core.$strip>>;
|
|
222
|
+
paging: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
223
|
+
next_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
224
|
+
}, z.core.$strip>>>;
|
|
222
225
|
}, z.core.$strip>;
|
|
223
226
|
readonly install_metrics_opens: z.ZodObject<{
|
|
224
227
|
results: z.ZodArray<z.ZodObject<{
|
|
225
|
-
|
|
228
|
+
timestamp: z.ZodString;
|
|
226
229
|
result: z.ZodObject<{
|
|
227
|
-
timestamp: z.ZodString;
|
|
228
230
|
last_attributed_touch_data_tilde_channel: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
229
231
|
last_attributed_touch_data_tilde_campaign: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
230
|
-
|
|
232
|
+
unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
|
|
231
233
|
}, z.core.$strip>;
|
|
232
234
|
}, z.core.$strip>>;
|
|
235
|
+
paging: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
236
|
+
next_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
237
|
+
}, z.core.$strip>>>;
|
|
233
238
|
}, z.core.$strip>;
|
|
234
239
|
readonly install_metrics_conversions: z.ZodObject<{
|
|
235
240
|
results: z.ZodArray<z.ZodObject<{
|
|
236
|
-
|
|
241
|
+
timestamp: z.ZodString;
|
|
237
242
|
result: z.ZodObject<{
|
|
238
|
-
timestamp: z.ZodString;
|
|
239
243
|
last_attributed_touch_data_tilde_channel: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
240
244
|
last_attributed_touch_data_tilde_campaign: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
241
|
-
|
|
245
|
+
unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
|
|
242
246
|
}, z.core.$strip>;
|
|
243
247
|
}, z.core.$strip>>;
|
|
248
|
+
paging: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
249
|
+
next_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
250
|
+
}, z.core.$strip>>>;
|
|
244
251
|
}, z.core.$strip>;
|
|
245
252
|
};
|
|
246
253
|
};
|
|
@@ -269,14 +276,17 @@ declare class BranchConnector extends BaseConnector<BranchSettings, BranchCreden
|
|
|
269
276
|
readonly responses: {
|
|
270
277
|
readonly deep_link_events: z.ZodObject<{
|
|
271
278
|
results: z.ZodArray<z.ZodObject<{
|
|
272
|
-
|
|
279
|
+
timestamp: z.ZodString;
|
|
273
280
|
result: z.ZodObject<{
|
|
274
|
-
timestamp: z.ZodString;
|
|
275
281
|
last_attributed_touch_data_tilde_channel: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
276
282
|
last_attributed_touch_data_tilde_campaign: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
277
283
|
last_attributed_touch_data_tilde_feature: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
284
|
+
unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
|
|
278
285
|
}, z.core.$strip>;
|
|
279
286
|
}, z.core.$strip>>;
|
|
287
|
+
paging: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
288
|
+
next_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
289
|
+
}, z.core.$strip>>>;
|
|
280
290
|
}, z.core.$strip>;
|
|
281
291
|
};
|
|
282
292
|
};
|
|
@@ -284,48 +294,57 @@ declare class BranchConnector extends BaseConnector<BranchSettings, BranchCreden
|
|
|
284
294
|
static readonly schemas: {
|
|
285
295
|
readonly install_metrics_installs: z.ZodObject<{
|
|
286
296
|
results: z.ZodArray<z.ZodObject<{
|
|
287
|
-
|
|
297
|
+
timestamp: z.ZodString;
|
|
288
298
|
result: z.ZodObject<{
|
|
289
|
-
timestamp: z.ZodString;
|
|
290
299
|
last_attributed_touch_data_tilde_channel: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
291
300
|
last_attributed_touch_data_tilde_campaign: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
292
|
-
|
|
301
|
+
unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
|
|
293
302
|
}, z.core.$strip>;
|
|
294
303
|
}, z.core.$strip>>;
|
|
304
|
+
paging: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
305
|
+
next_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
306
|
+
}, z.core.$strip>>>;
|
|
295
307
|
}, z.core.$strip>;
|
|
296
308
|
readonly install_metrics_opens: z.ZodObject<{
|
|
297
309
|
results: z.ZodArray<z.ZodObject<{
|
|
298
|
-
|
|
310
|
+
timestamp: z.ZodString;
|
|
299
311
|
result: z.ZodObject<{
|
|
300
|
-
timestamp: z.ZodString;
|
|
301
312
|
last_attributed_touch_data_tilde_channel: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
302
313
|
last_attributed_touch_data_tilde_campaign: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
303
|
-
|
|
314
|
+
unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
|
|
304
315
|
}, z.core.$strip>;
|
|
305
316
|
}, z.core.$strip>>;
|
|
317
|
+
paging: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
318
|
+
next_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
319
|
+
}, z.core.$strip>>>;
|
|
306
320
|
}, z.core.$strip>;
|
|
307
321
|
readonly install_metrics_conversions: z.ZodObject<{
|
|
308
322
|
results: z.ZodArray<z.ZodObject<{
|
|
309
|
-
|
|
323
|
+
timestamp: z.ZodString;
|
|
310
324
|
result: z.ZodObject<{
|
|
311
|
-
timestamp: z.ZodString;
|
|
312
325
|
last_attributed_touch_data_tilde_channel: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
313
326
|
last_attributed_touch_data_tilde_campaign: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
314
|
-
|
|
327
|
+
unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
|
|
315
328
|
}, z.core.$strip>;
|
|
316
329
|
}, z.core.$strip>>;
|
|
330
|
+
paging: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
331
|
+
next_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
332
|
+
}, z.core.$strip>>>;
|
|
317
333
|
}, z.core.$strip>;
|
|
318
334
|
} & {
|
|
319
335
|
readonly deep_link_events: z.ZodObject<{
|
|
320
336
|
results: z.ZodArray<z.ZodObject<{
|
|
321
|
-
|
|
337
|
+
timestamp: z.ZodString;
|
|
322
338
|
result: z.ZodObject<{
|
|
323
|
-
timestamp: z.ZodString;
|
|
324
339
|
last_attributed_touch_data_tilde_channel: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
325
340
|
last_attributed_touch_data_tilde_campaign: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
326
341
|
last_attributed_touch_data_tilde_feature: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
342
|
+
unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
|
|
327
343
|
}, z.core.$strip>;
|
|
328
344
|
}, z.core.$strip>>;
|
|
345
|
+
paging: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
346
|
+
next_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
347
|
+
}, z.core.$strip>>>;
|
|
329
348
|
}, z.core.$strip>;
|
|
330
349
|
} & Readonly<Record<string, z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>>;
|
|
331
350
|
static create(input: unknown, ctx?: ConnectorContext): BranchConnector;
|
package/dist/index.js
CHANGED
|
@@ -46,7 +46,7 @@ var doc = defineConnectorDoc({
|
|
|
46
46
|
displayName: "Branch",
|
|
47
47
|
category: "marketing",
|
|
48
48
|
brandColor: "#7CB833",
|
|
49
|
-
tagline: "Sync Branch install attribution metrics (installs, opens, conversions) and deep-link click events from the
|
|
49
|
+
tagline: "Sync Branch install attribution metrics (installs, opens, conversions) and deep-link click events from the Query API for mobile attribution dashboards.",
|
|
50
50
|
vendor: {
|
|
51
51
|
name: "Branch",
|
|
52
52
|
domain: "branch.io",
|
|
@@ -54,17 +54,17 @@ var doc = defineConnectorDoc({
|
|
|
54
54
|
website: "https://www.branch.io"
|
|
55
55
|
},
|
|
56
56
|
auth: {
|
|
57
|
-
summary: "A Branch app key and secret,
|
|
57
|
+
summary: "A Branch app key and secret, sent together in the Query API request body to authenticate each call.",
|
|
58
58
|
setup: [
|
|
59
59
|
"In the Branch dashboard, open Account Settings -> Profile and copy the Branch Key (starts with `key_live_`).",
|
|
60
60
|
"On the same screen, reveal and copy the Branch Secret (starts with `secret_live_`). Both values are app-scoped; keep them in a secret store.",
|
|
61
61
|
'Reference them from the connector config as `branchKey: secret("BRANCH_KEY")` and `branchSecret: secret("BRANCH_SECRET")`.'
|
|
62
62
|
]
|
|
63
63
|
},
|
|
64
|
-
rateLimit: "Branch
|
|
64
|
+
rateLimit: "The Branch Query API allows roughly 5 requests/second, 20/minute, and 150/hour per app. Because each sync splits its window into <=7-day segments and paginates, a wide window fans out to many requests; the connector relies on the shared HTTP client to honor 429 responses and the `Retry-After` header with backoff.",
|
|
65
65
|
limitations: [
|
|
66
|
-
"Daily granularity only - the connector requests `granularity=day` from the Branch
|
|
67
|
-
"
|
|
66
|
+
"Daily granularity only - the connector requests `granularity=day` from the Branch Query API to keep result cardinality bounded.",
|
|
67
|
+
"Branch rejects windows wider than 7 days, so each requested range is split into <=7-day segments and fetched one segment at a time.",
|
|
68
68
|
"Deep-link events are aggregated daily click counts per (date, channel, campaign, feature). Individual click-level records require the Branch Daily Export API which is intentionally out of scope."
|
|
69
69
|
]
|
|
70
70
|
});
|
|
@@ -84,51 +84,61 @@ var ANALYTICS_API_URL = "https://api2.branch.io/v1/query/analytics";
|
|
|
84
84
|
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
85
85
|
var DEFAULT_LOOKBACK_DAYS = 90;
|
|
86
86
|
var INCREMENTAL_LOOKBACK_DAYS = 14;
|
|
87
|
+
var MAX_WINDOW_DAYS = 7;
|
|
88
|
+
var PAGE_LIMIT = 1e3;
|
|
87
89
|
var INSTALL_METRIC_NAME = "branch_install_metrics";
|
|
88
90
|
var DEEP_LINK_EVENT_NAME = "branch_deep_link_event";
|
|
89
91
|
var CHANNEL_DIMENSION = "last_attributed_touch_data_tilde_channel";
|
|
90
92
|
var CAMPAIGN_DIMENSION = "last_attributed_touch_data_tilde_campaign";
|
|
91
93
|
var FEATURE_DIMENSION = "last_attributed_touch_data_tilde_feature";
|
|
92
|
-
var INSTALL_DATA_SOURCES = [
|
|
94
|
+
var INSTALL_DATA_SOURCES = [
|
|
95
|
+
"eo_install",
|
|
96
|
+
"eo_open",
|
|
97
|
+
"eo_custom_event"
|
|
98
|
+
];
|
|
93
99
|
var COUNT_FIELD_BY_DATA_SOURCE = {
|
|
94
100
|
eo_install: "installs",
|
|
95
101
|
eo_open: "opens",
|
|
96
|
-
|
|
102
|
+
eo_custom_event: "conversions"
|
|
97
103
|
};
|
|
98
|
-
var
|
|
104
|
+
var isoTimestampString = z.string().regex(
|
|
105
|
+
/^\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)?$/
|
|
106
|
+
);
|
|
99
107
|
var numericLike = z.union([z.number(), z.string(), z.null()]).optional();
|
|
108
|
+
var pagingSchema = z.object({ next_url: z.string().nullish() }).nullish();
|
|
100
109
|
var installResultRowSchema = z.object({
|
|
101
|
-
|
|
110
|
+
timestamp: isoTimestampString,
|
|
102
111
|
result: z.object({
|
|
103
|
-
timestamp: isoDateString,
|
|
104
112
|
[CHANNEL_DIMENSION]: z.string().nullish(),
|
|
105
113
|
[CAMPAIGN_DIMENSION]: z.string().nullish(),
|
|
106
|
-
|
|
114
|
+
unique_count: numericLike
|
|
107
115
|
})
|
|
108
116
|
});
|
|
109
117
|
var installResponseSchema = z.object({
|
|
110
|
-
results: z.array(installResultRowSchema)
|
|
118
|
+
results: z.array(installResultRowSchema),
|
|
119
|
+
paging: pagingSchema
|
|
111
120
|
});
|
|
112
121
|
var clickResultRowSchema = z.object({
|
|
113
|
-
|
|
122
|
+
timestamp: isoTimestampString,
|
|
114
123
|
result: z.object({
|
|
115
|
-
timestamp: isoDateString,
|
|
116
124
|
[CHANNEL_DIMENSION]: z.string().nullish(),
|
|
117
125
|
[CAMPAIGN_DIMENSION]: z.string().nullish(),
|
|
118
|
-
[FEATURE_DIMENSION]: z.string().nullish()
|
|
126
|
+
[FEATURE_DIMENSION]: z.string().nullish(),
|
|
127
|
+
unique_count: numericLike
|
|
119
128
|
})
|
|
120
129
|
});
|
|
121
130
|
var clickResponseSchema = z.object({
|
|
122
|
-
results: z.array(clickResultRowSchema)
|
|
131
|
+
results: z.array(clickResultRowSchema),
|
|
132
|
+
paging: pagingSchema
|
|
123
133
|
});
|
|
124
134
|
var branchResources = defineResources({
|
|
125
135
|
[INSTALL_METRIC_NAME]: {
|
|
126
136
|
shape: "metric",
|
|
127
|
-
description: "Daily Branch attribution metrics bucketed by channel and campaign. Primary value is `installs`; `opens
|
|
137
|
+
description: "Daily Branch attribution metrics bucketed by channel and campaign. Primary value is `installs`; `opens` and `conversions` are carried as attributes.",
|
|
128
138
|
endpoint: "POST /v1/query/analytics",
|
|
129
139
|
unit: "installs",
|
|
130
140
|
granularity: "day",
|
|
131
|
-
notes: "Merges three
|
|
141
|
+
notes: "Merges three Query API calls (data_source=eo_install, eo_open, eo_custom_event) keyed by (date, channel, campaign). Rows with missing channel or campaign are recorded as `null` for that attribute.",
|
|
132
142
|
dimensions: [
|
|
133
143
|
{ name: "date", description: "Calendar day of the metric sample (UTC)." },
|
|
134
144
|
{ name: "channel", description: "Branch last-attributed channel." },
|
|
@@ -137,11 +147,7 @@ var branchResources = defineResources({
|
|
|
137
147
|
{ name: "opens", description: "Attributed app opens on the day." },
|
|
138
148
|
{
|
|
139
149
|
name: "conversions",
|
|
140
|
-
description: "Attributed in-app
|
|
141
|
-
},
|
|
142
|
-
{
|
|
143
|
-
name: "costEstimated",
|
|
144
|
-
description: "Estimated cost in the app local currency (only populated for ad-network-integrated channels; 0 otherwise)."
|
|
150
|
+
description: "Attributed in-app custom-event conversions on the day."
|
|
145
151
|
}
|
|
146
152
|
],
|
|
147
153
|
responses: {
|
|
@@ -213,6 +219,21 @@ function isoDateToMs(date) {
|
|
|
213
219
|
}
|
|
214
220
|
return Date.UTC(y, m - 1, d);
|
|
215
221
|
}
|
|
222
|
+
function splitWindow(window) {
|
|
223
|
+
const fromMs = isoDateToMs(window.from);
|
|
224
|
+
const toMs = isoDateToMs(window.to);
|
|
225
|
+
if (!Number.isFinite(fromMs) || !Number.isFinite(toMs) || fromMs > toMs) {
|
|
226
|
+
return [window];
|
|
227
|
+
}
|
|
228
|
+
const segments = [];
|
|
229
|
+
let startMs = fromMs;
|
|
230
|
+
while (startMs <= toMs) {
|
|
231
|
+
const endMs = Math.min(startMs + (MAX_WINDOW_DAYS - 1) * MS_PER_DAY, toMs);
|
|
232
|
+
segments.push({ from: toIsoDate(startMs), to: toIsoDate(endMs) });
|
|
233
|
+
startMs = endMs + MS_PER_DAY;
|
|
234
|
+
}
|
|
235
|
+
return segments;
|
|
236
|
+
}
|
|
216
237
|
function parseNumber(value) {
|
|
217
238
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
218
239
|
return value;
|
|
@@ -234,7 +255,7 @@ function mergeInstallBuckets(rowsByDataSource) {
|
|
|
234
255
|
for (const dataSource of INSTALL_DATA_SOURCES) {
|
|
235
256
|
const field = COUNT_FIELD_BY_DATA_SOURCE[dataSource];
|
|
236
257
|
for (const row of rowsByDataSource[dataSource]) {
|
|
237
|
-
const date = normalizeDateBucket(row.
|
|
258
|
+
const date = normalizeDateBucket(row.timestamp);
|
|
238
259
|
const channel = row.result[CHANNEL_DIMENSION] ?? null;
|
|
239
260
|
const campaign = row.result[CAMPAIGN_DIMENSION] ?? null;
|
|
240
261
|
const key = bucketKey(date, channel, campaign);
|
|
@@ -246,12 +267,11 @@ function mergeInstallBuckets(rowsByDataSource) {
|
|
|
246
267
|
campaign,
|
|
247
268
|
installs: 0,
|
|
248
269
|
opens: 0,
|
|
249
|
-
conversions: 0
|
|
250
|
-
costEstimated: 0
|
|
270
|
+
conversions: 0
|
|
251
271
|
};
|
|
252
272
|
buckets.set(key, bucket);
|
|
253
273
|
}
|
|
254
|
-
const count = parseNumber(row.unique_count);
|
|
274
|
+
const count = parseNumber(row.result.unique_count);
|
|
255
275
|
if (field === "installs") {
|
|
256
276
|
bucket.installs += count;
|
|
257
277
|
} else if (field === "opens") {
|
|
@@ -259,9 +279,6 @@ function mergeInstallBuckets(rowsByDataSource) {
|
|
|
259
279
|
} else {
|
|
260
280
|
bucket.conversions += count;
|
|
261
281
|
}
|
|
262
|
-
if (dataSource === "eo_install") {
|
|
263
|
-
bucket.costEstimated += parseNumber(row.result.cost_in_local_currency);
|
|
264
|
-
}
|
|
265
282
|
}
|
|
266
283
|
}
|
|
267
284
|
return Array.from(buckets.values()).sort(
|
|
@@ -280,18 +297,17 @@ function installBucketToMetricSample(bucket) {
|
|
|
280
297
|
campaign: bucket.campaign,
|
|
281
298
|
installs: bucket.installs,
|
|
282
299
|
opens: bucket.opens,
|
|
283
|
-
conversions: bucket.conversions
|
|
284
|
-
costEstimated: bucket.costEstimated
|
|
300
|
+
conversions: bucket.conversions
|
|
285
301
|
}
|
|
286
302
|
};
|
|
287
303
|
}
|
|
288
304
|
function clickRowToEventRecord(row) {
|
|
289
|
-
const date = normalizeDateBucket(row.
|
|
305
|
+
const date = normalizeDateBucket(row.timestamp);
|
|
290
306
|
const channel = row.result[CHANNEL_DIMENSION] ?? null;
|
|
291
307
|
const campaign = row.result[CAMPAIGN_DIMENSION] ?? null;
|
|
292
308
|
const feature = row.result[FEATURE_DIMENSION] ?? null;
|
|
293
309
|
const ts = isoDateToMs(date);
|
|
294
|
-
const clicks = parseNumber(row.unique_count);
|
|
310
|
+
const clicks = parseNumber(row.result.unique_count);
|
|
295
311
|
const startTs = Number.isFinite(ts) ? ts : 0;
|
|
296
312
|
return {
|
|
297
313
|
name: DEEP_LINK_EVENT_NAME,
|
|
@@ -340,59 +356,85 @@ var BranchConnector = class _BranchConnector extends BaseConnector {
|
|
|
340
356
|
granularity: "day",
|
|
341
357
|
aggregation: "unique_count",
|
|
342
358
|
ordered: "ascending",
|
|
343
|
-
ordered_by: "timestamp"
|
|
359
|
+
ordered_by: "timestamp",
|
|
360
|
+
limit: PAGE_LIMIT
|
|
344
361
|
});
|
|
345
362
|
}
|
|
346
363
|
async fetchAggregate(resource, dataSource, dimensions, window, signal) {
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
364
|
+
const body = this.buildBody(dataSource, dimensions, window);
|
|
365
|
+
const results = [];
|
|
366
|
+
const base = new URL(ANALYTICS_API_URL);
|
|
367
|
+
const visited = /* @__PURE__ */ new Set();
|
|
368
|
+
let url = ANALYTICS_API_URL;
|
|
369
|
+
while (!visited.has(url)) {
|
|
370
|
+
visited.add(url);
|
|
371
|
+
const res = await this.post(url, {
|
|
372
|
+
resource,
|
|
373
|
+
headers: this.buildHeaders(),
|
|
374
|
+
body,
|
|
375
|
+
signal
|
|
376
|
+
});
|
|
377
|
+
results.push(...res.body.results ?? []);
|
|
378
|
+
const next = res.body.paging?.next_url;
|
|
379
|
+
if (!next) {
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
const parsedNext = new URL(next, ANALYTICS_API_URL);
|
|
383
|
+
if (parsedNext.origin !== base.origin || parsedNext.pathname !== base.pathname) {
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
url = parsedNext.toString();
|
|
387
|
+
}
|
|
388
|
+
return results;
|
|
354
389
|
}
|
|
355
|
-
async fetchInstallBuckets(
|
|
390
|
+
async fetchInstallBuckets(segments, signal) {
|
|
356
391
|
const dims = [CHANNEL_DIMENSION, CAMPAIGN_DIMENSION];
|
|
357
392
|
const rowsByDataSource = {
|
|
358
393
|
eo_install: [],
|
|
359
394
|
eo_open: [],
|
|
360
|
-
|
|
395
|
+
eo_custom_event: []
|
|
361
396
|
};
|
|
362
|
-
for (const
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
397
|
+
for (const segment of segments) {
|
|
398
|
+
for (const dataSource of INSTALL_DATA_SOURCES) {
|
|
399
|
+
const field = COUNT_FIELD_BY_DATA_SOURCE[dataSource];
|
|
400
|
+
const tag = `install_metrics_${field}`;
|
|
401
|
+
const rows = await this.fetchAggregate(
|
|
402
|
+
tag,
|
|
403
|
+
dataSource,
|
|
404
|
+
dims,
|
|
405
|
+
segment,
|
|
406
|
+
signal
|
|
407
|
+
);
|
|
408
|
+
rowsByDataSource[dataSource].push(...rows);
|
|
409
|
+
}
|
|
373
410
|
}
|
|
374
411
|
return mergeInstallBuckets(rowsByDataSource);
|
|
375
412
|
}
|
|
376
|
-
async fetchClickRows(
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
413
|
+
async fetchClickRows(segments, signal) {
|
|
414
|
+
const rows = [];
|
|
415
|
+
for (const segment of segments) {
|
|
416
|
+
const segmentRows = await this.fetchAggregate(
|
|
417
|
+
"deep_link_events",
|
|
418
|
+
"eo_click",
|
|
419
|
+
[CHANNEL_DIMENSION, CAMPAIGN_DIMENSION, FEATURE_DIMENSION],
|
|
420
|
+
segment,
|
|
421
|
+
signal
|
|
422
|
+
);
|
|
423
|
+
rows.push(...segmentRows);
|
|
424
|
+
}
|
|
425
|
+
return rows;
|
|
385
426
|
}
|
|
386
427
|
async writePhase(storage, phase, window, signal) {
|
|
428
|
+
const segments = splitWindow(window);
|
|
387
429
|
if (phase === "install_metrics") {
|
|
388
|
-
const buckets = await this.fetchInstallBuckets(
|
|
430
|
+
const buckets = await this.fetchInstallBuckets(segments, signal);
|
|
389
431
|
await storage.metrics([], { names: [INSTALL_METRIC_NAME] });
|
|
390
432
|
for (const bucket of buckets) {
|
|
391
433
|
await storage.metric(installBucketToMetricSample(bucket));
|
|
392
434
|
}
|
|
393
435
|
return;
|
|
394
436
|
}
|
|
395
|
-
const rows = await this.fetchClickRows(
|
|
437
|
+
const rows = await this.fetchClickRows(segments, signal);
|
|
396
438
|
for (const row of rows) {
|
|
397
439
|
await storage.event(clickRowToEventRecord(row));
|
|
398
440
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../connector-shared/src/errors.ts","../../../connector-shared/src/retry.ts","../../../connector-shared/src/version.ts","../../../connector-shared/src/request.ts","../../../connector-shared/src/rate-limit.ts","../../../connector-shared/src/map-concurrent.ts","../../../connector-shared/src/sanitize.ts","../../../connector-shared/src/epoch.ts","../../../connector-shared/src/pagination.ts","../../../connector-shared/src/logger.ts","../src/branch.ts","../src/index.ts"],"sourcesContent":["import type { HttpResponse } from './types';\n\nexport type HttpErrorKind =\n | 'transient'\n | 'rate_limit'\n | 'auth'\n | 'upstream_bug'\n | 'client_bug';\n\nexport abstract class HttpClientError extends Error {\n abstract readonly kind: HttpErrorKind;\n readonly response?: HttpResponse;\n\n constructor(message: string, response?: HttpResponse) {\n super(message);\n this.name = new.target.name;\n this.response = response;\n }\n}\n\nexport class TransientError extends HttpClientError {\n readonly kind = 'transient' as const;\n}\n\nexport class RateLimitError extends HttpClientError {\n readonly kind = 'rate_limit' as const;\n readonly retryAfter?: Date;\n\n constructor(message: string, response?: HttpResponse, retryAfter?: Date) {\n super(message, response);\n this.retryAfter = retryAfter;\n }\n}\n\nexport class AuthError extends HttpClientError {\n readonly kind = 'auth' as const;\n}\n\nexport class UpstreamBugError extends HttpClientError {\n readonly kind = 'upstream_bug' as const;\n}\n\nexport class ClientBugError extends HttpClientError {\n readonly kind = 'client_bug' as const;\n}\n\nexport function classifyStatus(status: number): HttpErrorKind {\n if (status === 429) {\n return 'rate_limit';\n }\n if (status === 401 || status === 403) {\n return 'auth';\n }\n if (status === 408) {\n return 'transient';\n }\n if (status >= 500) {\n return 'upstream_bug';\n }\n if (status >= 400) {\n return 'client_bug';\n }\n return 'client_bug';\n}\n\nexport function errorForStatus(\n message: string,\n response: HttpResponse,\n retryAfter?: Date,\n): HttpClientError {\n const kind = classifyStatus(response.status);\n switch (kind) {\n case 'rate_limit':\n return new RateLimitError(message, response, retryAfter);\n case 'auth':\n return new AuthError(message, response);\n case 'transient':\n return new TransientError(message, response);\n case 'upstream_bug':\n return new UpstreamBugError(message, response);\n case 'client_bug':\n return new ClientBugError(message, response);\n }\n}\n","import { HttpClientError, RateLimitError, TransientError } from './errors';\n\nexport interface RetryPolicy {\n maxAttempts?: number;\n initialDelayMs?: number;\n maxDelayMs?: number;\n retryOn?: (status: number | null, err?: Error) => boolean;\n}\n\nexport const defaultRetryOn = (status: number | null, err?: Error): boolean => {\n if (err instanceof RateLimitError) {\n return true;\n }\n if (err instanceof TransientError) {\n return true;\n }\n if (status === null) {\n return err instanceof Error && !(err instanceof HttpClientError);\n }\n if (status === 408 || status === 429) {\n return true;\n }\n if (status >= 500) {\n return true;\n }\n return false;\n};\n\nexport function backoffDelayMs(\n attempt: number,\n policy: Required<Pick<RetryPolicy, 'initialDelayMs' | 'maxDelayMs'>>,\n): number {\n const base = policy.initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, policy.maxDelayMs);\n}\n\nexport function parseRetryAfter(\n headerValue: string | null,\n now: Date = new Date(),\n): Date | undefined {\n if (!headerValue) {\n return undefined;\n }\n const trimmed = headerValue.trim();\n if (/^\\d+$/.test(trimmed)) {\n return new Date(now.getTime() + Number(trimmed) * 1000);\n }\n const parsed = Date.parse(trimmed);\n if (Number.isNaN(parsed)) {\n return undefined;\n }\n return new Date(parsed);\n}\n\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) {\n return Promise.reject(signal.reason ?? new Error('Aborted'));\n }\n return new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal!.reason ?? new Error('Aborted'));\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n","export const HTTP_CLIENT_VERSION = '0.0.0';\n\nexport const DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n\nexport function connectorUserAgent(connectorId: string): string {\n return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n}\n","import {\n AuthError,\n ClientBugError,\n HttpClientError,\n RateLimitError,\n TransientError,\n UpstreamBugError,\n errorForStatus,\n} from './errors';\nimport { defaultRetryOn, parseRetryAfter, sleep } from './retry';\nimport type { FetchLike, HttpMethod, HttpRequest, HttpResponse } from './types';\nimport { DEFAULT_USER_AGENT } from './version';\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_ATTEMPTS = 3;\nconst DEFAULT_INITIAL_DELAY_MS = 1000;\nconst DEFAULT_MAX_DELAY_MS = 60_000;\nconst OBSERVER_TIMEOUT_MS = 250;\n\nexport interface RequestObservation {\n url: string;\n method: HttpMethod;\n status: number;\n resource: string;\n requestId: string;\n body: unknown;\n}\n\nexport type RequestObserver = (\n event: RequestObservation,\n) => void | Promise<void>;\n\nexport interface RequestOptions {\n fetch?: FetchLike;\n observer?: RequestObserver;\n resource: string;\n requestId?: string;\n}\n\nasync function notifyObserver(\n observer: RequestObserver,\n event: RequestObservation,\n): Promise<void> {\n let result: void | Promise<void>;\n try {\n result = observer(event);\n } catch (err) {\n console.warn('[connector-shared] request observer threw:', err);\n return;\n }\n if (!(result instanceof Promise)) {\n return;\n }\n const guarded = result.catch((err) => {\n console.warn('[connector-shared] request observer rejected:', err);\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, OBSERVER_TIMEOUT_MS);\n });\n try {\n await Promise.race([guarded, timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\nfunction newRequestId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) {\n return c.randomUUID();\n }\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction mergeHeaders(\n defaults: Record<string, string>,\n overrides: Record<string, string> | undefined,\n): Record<string, string> {\n const merged: Record<string, string> = {};\n for (const [k, v] of Object.entries(defaults)) {\n merged[k.toLowerCase()] = v;\n }\n if (overrides) {\n for (const [k, v] of Object.entries(overrides)) {\n merged[k.toLowerCase()] = v;\n }\n }\n return merged;\n}\n\nfunction linkTimeoutSignal(\n parent: AbortSignal | undefined,\n timeoutMs: number,\n): { signal: AbortSignal; cancel: () => void } {\n const controller = new AbortController();\n const onParentAbort = () => {\n controller.abort(parent?.reason);\n };\n if (parent) {\n if (parent.aborted) {\n controller.abort(parent.reason);\n } else {\n parent.addEventListener('abort', onParentAbort, { once: true });\n }\n }\n const timer = setTimeout(() => {\n controller.abort(new Error(`Request timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n return {\n signal: controller.signal,\n cancel: () => {\n clearTimeout(timer);\n if (parent) {\n parent.removeEventListener('abort', onParentAbort);\n }\n },\n };\n}\n\nasync function readBody(res: Response, parseJson: boolean): Promise<unknown> {\n if (res.status === 204 || res.status === 205) {\n return null;\n }\n const contentType = res.headers.get('content-type') ?? '';\n if (parseJson && contentType.includes('application/json')) {\n const text = await res.text();\n if (text.length === 0) {\n return null;\n }\n return JSON.parse(text);\n }\n return res.text();\n}\n\nexport async function request<T = unknown>(\n req: HttpRequest,\n options: RequestOptions,\n): Promise<HttpResponse<T>> {\n const fetchImpl: FetchLike = options.fetch ?? (globalThis.fetch as FetchLike);\n const retry = req.retry ?? {};\n const maxAttempts = retry.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const initialDelayMs = retry.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;\n const maxDelayMs = retry.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;\n const retryOn = retry.retryOn ?? defaultRetryOn;\n const timeoutMs = req.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const parseJson = req.parseJson ?? true;\n\n const headers = mergeHeaders(\n {\n 'User-Agent': DEFAULT_USER_AGENT,\n Accept: 'application/json',\n },\n req.headers,\n );\n\n let lastErr: Error | undefined;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n req.signal?.throwIfAborted();\n\n const { signal, cancel } = linkTimeoutSignal(req.signal, timeoutMs);\n let res: Response;\n try {\n res = await fetchImpl(req.url, {\n method: req.method ?? 'GET',\n headers,\n body: req.body as RequestInit['body'],\n signal,\n });\n } catch (err) {\n cancel();\n if (req.signal?.aborted) {\n throw req.signal.reason ?? err;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n lastErr = error;\n if (attempt < maxAttempts - 1 && retryOn(null, error)) {\n const delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n await sleep(delay, req.signal);\n continue;\n }\n throw new TransientError(error.message);\n }\n cancel();\n\n const body = await readBody(res, parseJson);\n const httpResponse: HttpResponse<T> = {\n status: res.status,\n headers: res.headers,\n body: body as T,\n };\n if (req.rateLimit) {\n const state = req.rateLimit.parse(res.headers);\n if (state) {\n httpResponse.rateLimitState = state;\n }\n }\n\n if (options.observer) {\n await notifyObserver(options.observer, {\n url: req.url,\n method: req.method ?? 'GET',\n status: res.status,\n resource: options.resource,\n requestId: options.requestId ?? newRequestId(),\n body,\n });\n }\n\n if (res.ok) {\n return httpResponse;\n }\n\n const retryAfter = parseRetryAfter(res.headers.get('retry-after'));\n const message = `HTTP ${res.status} ${res.statusText} for ${req.method ?? 'GET'} ${req.url}`;\n const err = errorForStatus(message, httpResponse, retryAfter);\n\n if (\n attempt < maxAttempts - 1 &&\n retryOn(res.status, err) &&\n !(err instanceof AuthError) &&\n !(err instanceof ClientBugError)\n ) {\n lastErr = err;\n let delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n if (err instanceof RateLimitError && retryAfter) {\n const wait = retryAfter.getTime() - Date.now();\n if (wait > 0) {\n delay = Math.min(wait, maxDelayMs);\n }\n }\n await sleep(delay, req.signal);\n continue;\n }\n\n throw err;\n }\n\n throw lastErr ?? new UpstreamBugError('Exhausted retry attempts');\n}\n\nfunction computeDelay(\n attempt: number,\n initialDelayMs: number,\n maxDelayMs: number,\n): number {\n const base = initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, maxDelayMs);\n}\n\nexport { HttpClientError };\n","export interface RateLimitState {\n remaining: number;\n resetAt: Date;\n}\n\nexport interface RateLimitPolicy {\n parse(headers: Headers): RateLimitState | null;\n}\n\nexport interface StandardRateLimitPolicyConfig {\n remainingHeader: string;\n resetHeader: string;\n resetUnit: 's' | 'ms';\n resetFallbackMs?: number;\n}\n\nexport function standardRateLimitPolicy(\n config: StandardRateLimitPolicyConfig,\n): RateLimitPolicy {\n const { remainingHeader, resetHeader, resetUnit, resetFallbackMs } = config;\n const multiplier = resetUnit === 's' ? 1000 : 1;\n return {\n parse(h) {\n const remainingRaw = h.get(remainingHeader);\n if (remainingRaw === null || remainingRaw.trim() === '') {\n return null;\n }\n const remaining = Number(remainingRaw);\n if (!Number.isFinite(remaining)) {\n return null;\n }\n const resetRaw = h.get(resetHeader);\n if (resetRaw === null) {\n if (resetFallbackMs === undefined) {\n return null;\n }\n return {\n remaining,\n resetAt: new Date(Date.now() + resetFallbackMs),\n };\n }\n if (resetRaw.trim() === '') {\n return null;\n }\n const reset = Number(resetRaw);\n if (!Number.isFinite(reset) || reset < 0) {\n return null;\n }\n const resetMs = reset * multiplier;\n if (!Number.isFinite(resetMs)) {\n return null;\n }\n return { remaining, resetAt: new Date(resetMs) };\n },\n };\n}\n","export async function mapWithConcurrency<T, R>(\n items: readonly T[],\n concurrency: number,\n fn: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n const results = new Array<R>(items.length);\n if (items.length === 0) {\n return results;\n }\n const normalized = Number.isFinite(concurrency) ? Math.floor(concurrency) : 1;\n const limit = Math.max(1, Math.min(normalized, items.length));\n let next = 0;\n let failed = false;\n\n async function worker(): Promise<void> {\n while (!failed) {\n const i = next++;\n if (i >= items.length) {\n return;\n }\n try {\n results[i] = await fn(items[i]!, i);\n } catch (err) {\n failed = true;\n throw err;\n }\n }\n }\n\n const workers: Promise<void>[] = [];\n for (let w = 0; w < limit; w++) {\n workers.push(worker());\n }\n await Promise.all(workers);\n return results;\n}\n","export interface SanitizeAllowedUrlOptions {\n url: string | null;\n host: string;\n pathname: string;\n protocol?: 'https:' | 'http:';\n}\n\nexport function sanitizeAllowedUrl(\n options: SanitizeAllowedUrlOptions,\n): string | null {\n const { url, host, pathname, protocol = 'https:' } = options;\n if (url === null) {\n return null;\n }\n try {\n const u = new URL(url);\n if (u.protocol !== protocol || u.host !== host || u.pathname !== pathname) {\n return null;\n }\n return u.toString();\n } catch {\n return null;\n }\n}\n","export type EpochUnit = 'ms' | 's' | 'iso';\n\nexport function parseEpoch(\n value: number | string | null | undefined,\n unit: EpochUnit,\n): number | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (unit === 'iso') {\n if (typeof value !== 'string') {\n return null;\n }\n const ms = new Date(value).getTime();\n return Number.isFinite(ms) ? ms : null;\n }\n if (typeof value === 'string' && value.trim() === '') {\n return null;\n }\n const n = typeof value === 'number' ? value : Number(value);\n if (!Number.isFinite(n)) {\n return null;\n }\n const result = unit === 's' ? n * 1000 : n;\n return Number.isFinite(result) ? result : null;\n}\n","import { request } from './request';\nimport type { HttpRequest } from './types';\n\nexport function parseLinkHeader(header: string | null): Record<string, string> {\n if (!header) {\n return {};\n }\n const result: Record<string, string> = {};\n for (const part of header.split(',')) {\n const match = part.match(/<([^>]+)>\\s*;\\s*rel=\"([^\"]+)\"/);\n if (match) {\n result[match[2]!] = match[1]!;\n }\n }\n return result;\n}\n\nexport async function* paginateLink<T>(\n initial: HttpRequest,\n parse: (body: unknown) => T[],\n options: { resource: string },\n): AsyncIterable<T> {\n let next: string | null = initial.url;\n while (next) {\n const res: Awaited<ReturnType<typeof request>> = await request(\n {\n ...initial,\n url: next,\n },\n { resource: options.resource },\n );\n for (const item of parse(res.body)) {\n yield item;\n }\n const links = parseLinkHeader(res.headers.get('link'));\n next = links['next'] ?? null;\n }\n}\n\nexport async function* paginateCursor<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; nextCursor: string | null },\n buildNext: (req: HttpRequest, cursor: string) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let req: HttpRequest = initial;\n while (true) {\n const res = await request(req, { resource: options.resource });\n const { items, nextCursor } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!nextCursor) {\n return;\n }\n req = buildNext(req, nextCursor);\n }\n}\n\nexport async function* paginatePage<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; hasMore: boolean },\n buildPage: (req: HttpRequest, page: number) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let page = 1;\n while (true) {\n const req = page === 1 ? initial : buildPage(initial, page);\n const res = await request(req, { resource: options.resource });\n const { items, hasMore } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!hasMore || items.length === 0) {\n return;\n }\n page++;\n }\n}\n","export type LogFields = Record<string, unknown>;\n\nexport interface ConnectorLogger {\n info(event: string, fields?: LogFields): void;\n warn(event: string, fields?: LogFields): void;\n}\n\nexport interface ConnectorLoggerOptions {\n scope: string;\n}\n\nconst MAX_VALUE_LEN = 120;\n\nfunction truncate(s: string, max = MAX_VALUE_LEN): string {\n if (s.length <= max) {\n return s;\n }\n return `${s.slice(0, max - 1)}…`;\n}\n\nfunction formatValue(value: unknown): string {\n if (value === null) {\n return 'null';\n }\n if (value === undefined) {\n return '';\n }\n if (typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n if (typeof value === 'string') {\n const t = truncate(value);\n if (/[\\s\"=]/.test(t)) {\n return JSON.stringify(t);\n }\n return t;\n }\n if (typeof value === 'bigint') {\n return value.toString();\n }\n let json: string | undefined;\n try {\n json = JSON.stringify(value);\n } catch {\n json = undefined;\n }\n return truncate(json ?? String(value));\n}\n\nexport function formatLogFields(fields?: LogFields): string {\n if (!fields) {\n return '';\n }\n const parts: string[] = [];\n for (const [k, v] of Object.entries(fields)) {\n if (v === undefined) {\n continue;\n }\n parts.push(`${k}=${formatValue(v)}`);\n }\n return parts.length > 0 ? ` ${parts.join(' ')}` : '';\n}\n\nexport function formatLogLine(\n scope: string,\n event: string,\n fields?: LogFields,\n): string {\n return `[${scope}] ${event}${formatLogFields(fields)}`;\n}\n\nexport function createDefaultConnectorLogger(\n opts: ConnectorLoggerOptions,\n): ConnectorLogger {\n return {\n info(event, fields) {\n console.info(formatLogLine(opts.scope, event, fields));\n },\n warn(event, fields) {\n console.warn(formatLogLine(opts.scope, event, fields));\n },\n };\n}\n\nconst NOOP_LOGGER: ConnectorLogger = {\n info() {},\n warn() {},\n};\n\nexport function noopConnectorLogger(): ConnectorLogger {\n return NOOP_LOGGER;\n}\n","import { connectorUserAgent } from '@rawdash/connector-shared';\nimport {\n BaseConnector,\n type ChunkedSyncCursor,\n type ConnectorContext,\n type ConnectorDoc,\n type CredentialsSchema,\n type Event,\n type MetricSample,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\n defineConnectorDoc,\n defineResources,\n makeChunkedCursorGuard,\n paginateChunked,\n schemasFromResources,\n selectActivePhases,\n} from '@rawdash/core';\nimport { z } from 'zod';\n\nexport const configFields = defineConfigFields(\n z.object({\n branchKey: z.object({ $secret: z.string() }).meta({\n label: 'Branch key',\n description:\n 'Your Branch app key (starts with `key_live_`). Find it in the Branch dashboard under Account Settings -> Profile.',\n placeholder: 'key_live_xxxxxxxxxxxxxxxxxxxxxxxxxx',\n secret: true,\n }),\n branchSecret: z.object({ $secret: z.string() }).meta({\n label: 'Branch secret',\n description:\n 'Your Branch app secret (starts with `secret_live_`). Find it next to the key in the Branch dashboard.',\n placeholder: 'secret_live_xxxxxxxxxxxxxxxxxxxxxxxxxx',\n secret: true,\n }),\n lookbackDays: z.number().int().positive().optional().meta({\n label: 'Lookback days (full sync)',\n description:\n 'How many calendar days of metrics/events to fetch on a full sync. Defaults to 90.',\n placeholder: '90',\n }),\n resources: z\n .array(z.enum(['install_metrics', 'deep_link_events']))\n .nonempty()\n .optional()\n .meta({\n label: 'Resources',\n description:\n 'Which Branch resources to sync. Omit to sync all of them.',\n }),\n }),\n);\n\nexport const doc: ConnectorDoc = defineConnectorDoc({\n displayName: 'Branch',\n category: 'marketing',\n brandColor: '#7CB833',\n tagline:\n 'Sync Branch install attribution metrics (installs, opens, conversions) and deep-link click events from the Cross-Platform Analytics API for mobile attribution dashboards.',\n vendor: {\n name: 'Branch',\n domain: 'branch.io',\n apiDocs: 'https://help.branch.io/developers-hub/reference',\n website: 'https://www.branch.io',\n },\n auth: {\n summary:\n 'A Branch app key and secret, used together to authenticate Cross-Platform Analytics API requests.',\n setup: [\n 'In the Branch dashboard, open Account Settings -> Profile and copy the Branch Key (starts with `key_live_`).',\n 'On the same screen, reveal and copy the Branch Secret (starts with `secret_live_`). Both values are app-scoped; keep them in a secret store.',\n 'Reference them from the connector config as `branchKey: secret(\"BRANCH_KEY\")` and `branchSecret: secret(\"BRANCH_SECRET\")`.',\n ],\n },\n rateLimit:\n 'Branch enforces a per-app request quota on the Cross-Platform Analytics API (roughly 1 request/second). The connector issues one POST per data source per resource per sync and respects 429 + Retry-After backoff via the shared HTTP client.',\n limitations: [\n 'Daily granularity only - the connector requests `granularity=day` from the Branch Aggregate API to keep result cardinality bounded.',\n 'Cost attribution is best-effort - Branch only exposes `cost_in_local_currency` for ad-network-integrated channels. Rows without cost data carry `costEstimated: 0`.',\n 'Deep-link events are aggregated daily click counts per (date, channel, campaign, feature). Individual click-level records require the Branch Daily Export API which is intentionally out of scope.',\n ],\n});\n\nexport interface BranchSettings {\n lookbackDays?: number;\n resources?: readonly BranchResource[];\n}\n\nconst branchCredentials = {\n branchKey: {\n description: 'Branch app key (key_live_...)',\n auth: 'required' as const,\n },\n branchSecret: {\n description: 'Branch app secret (secret_live_...)',\n auth: 'required' as const,\n },\n} satisfies CredentialsSchema;\n\ntype BranchCredentials = typeof branchCredentials;\n\nconst PHASE_ORDER = ['install_metrics', 'deep_link_events'] as const;\n\ntype BranchPhase = (typeof PHASE_ORDER)[number];\n\nexport type BranchResource = BranchPhase;\n\ntype BranchSyncCursor = ChunkedSyncCursor<BranchPhase, string>;\n\nconst isBranchSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);\n\nconst ANALYTICS_API_URL = 'https://api2.branch.io/v1/query/analytics';\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\nconst DEFAULT_LOOKBACK_DAYS = 90;\nconst INCREMENTAL_LOOKBACK_DAYS = 14;\n\nconst INSTALL_METRIC_NAME = 'branch_install_metrics';\nconst DEEP_LINK_EVENT_NAME = 'branch_deep_link_event';\n\nconst CHANNEL_DIMENSION = 'last_attributed_touch_data_tilde_channel';\nconst CAMPAIGN_DIMENSION = 'last_attributed_touch_data_tilde_campaign';\nconst FEATURE_DIMENSION = 'last_attributed_touch_data_tilde_feature';\n\nconst INSTALL_DATA_SOURCES = ['eo_install', 'eo_open', 'eo_event'] as const;\ntype InstallDataSource = (typeof INSTALL_DATA_SOURCES)[number];\n\nconst COUNT_FIELD_BY_DATA_SOURCE: Record<InstallDataSource, string> = {\n eo_install: 'installs',\n eo_open: 'opens',\n eo_event: 'conversions',\n};\n\nconst isoDateString = z.string().regex(/^\\d{4}-\\d{2}-\\d{2}$/);\nconst numericLike = z.union([z.number(), z.string(), z.null()]).optional();\n\nconst installResultRowSchema = z.object({\n unique_count: numericLike,\n result: z.object({\n timestamp: isoDateString,\n [CHANNEL_DIMENSION]: z.string().nullish(),\n [CAMPAIGN_DIMENSION]: z.string().nullish(),\n cost_in_local_currency: numericLike,\n }),\n});\n\nconst installResponseSchema = z.object({\n results: z.array(installResultRowSchema),\n});\n\nconst clickResultRowSchema = z.object({\n unique_count: numericLike,\n result: z.object({\n timestamp: isoDateString,\n [CHANNEL_DIMENSION]: z.string().nullish(),\n [CAMPAIGN_DIMENSION]: z.string().nullish(),\n [FEATURE_DIMENSION]: z.string().nullish(),\n }),\n});\n\nconst clickResponseSchema = z.object({\n results: z.array(clickResultRowSchema),\n});\n\nexport const branchResources = defineResources({\n [INSTALL_METRIC_NAME]: {\n shape: 'metric',\n description:\n 'Daily Branch attribution metrics bucketed by channel and campaign. Primary value is `installs`; `opens`, `conversions`, and `costEstimated` are carried as attributes.',\n endpoint: 'POST /v1/query/analytics',\n unit: 'installs',\n granularity: 'day',\n notes:\n 'Merges three Aggregate API calls (data_source=eo_install, eo_open, eo_event) keyed by (date, channel, campaign). Rows with missing channel or campaign are recorded as `null` for that attribute.',\n dimensions: [\n { name: 'date', description: 'Calendar day of the metric sample (UTC).' },\n { name: 'channel', description: 'Branch last-attributed channel.' },\n { name: 'campaign', description: 'Branch last-attributed campaign.' },\n { name: 'installs', description: 'Attributed installs on the day.' },\n { name: 'opens', description: 'Attributed app opens on the day.' },\n {\n name: 'conversions',\n description: 'Attributed in-app conversion events on the day.',\n },\n {\n name: 'costEstimated',\n description:\n 'Estimated cost in the app local currency (only populated for ad-network-integrated channels; 0 otherwise).',\n },\n ],\n responses: {\n install_metrics_installs: installResponseSchema,\n install_metrics_opens: installResponseSchema,\n install_metrics_conversions: installResponseSchema,\n },\n },\n [DEEP_LINK_EVENT_NAME]: {\n shape: 'event',\n description:\n 'Daily aggregated Branch deep-link click events bucketed by channel, campaign, and feature. One event per (date, channel, campaign, feature) row carrying the daily click count.',\n endpoint: 'POST /v1/query/analytics',\n notes:\n 'Sourced from data_source=eo_click. Event id encodes the bucket so resyncs are idempotent.',\n fields: [\n { name: 'date', description: 'Calendar day of the click bucket (UTC).' },\n { name: 'channel', description: 'Branch last-attributed channel.' },\n { name: 'campaign', description: 'Branch last-attributed campaign.' },\n {\n name: 'feature',\n description: 'Branch last-attributed feature (e.g. `sharing`).',\n },\n { name: 'clicks', description: 'Click count for the bucket.' },\n ],\n filterable: [],\n responses: { deep_link_events: clickResponseSchema },\n },\n});\n\nexport type BranchInstallResultRow = z.infer<typeof installResultRowSchema>;\nexport type BranchClickResultRow = z.infer<typeof clickResultRowSchema>;\n\ninterface BranchWindow {\n from: string;\n to: string;\n}\n\nfunction pad2(n: number): string {\n return String(n).padStart(2, '0');\n}\n\nfunction toIsoDate(ms: number): string {\n const d = new Date(ms);\n return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;\n}\n\nfunction startOfUtcDay(ms: number): number {\n return Math.floor(ms / MS_PER_DAY) * MS_PER_DAY;\n}\n\nexport function getWindow(\n options: SyncOptions,\n lookbackDays: number,\n now: number = Date.now(),\n): BranchWindow {\n const today = startOfUtcDay(now);\n if (options.mode === 'latest') {\n return {\n from: toIsoDate(today - (INCREMENTAL_LOOKBACK_DAYS - 1) * MS_PER_DAY),\n to: toIsoDate(today),\n };\n }\n if (options.since) {\n const sinceMs = new Date(options.since).getTime();\n if (Number.isFinite(sinceMs)) {\n const requested = Math.max(\n 1,\n Math.ceil((today - startOfUtcDay(sinceMs)) / MS_PER_DAY) + 1,\n );\n const capped = Math.min(requested, lookbackDays);\n return {\n from: toIsoDate(today - (capped - 1) * MS_PER_DAY),\n to: toIsoDate(today),\n };\n }\n }\n return {\n from: toIsoDate(today - (lookbackDays - 1) * MS_PER_DAY),\n to: toIsoDate(today),\n };\n}\n\nfunction isoDateToMs(date: string): number {\n const [y, m, d] = date.split('-').map((part) => Number(part));\n if (\n y === undefined ||\n m === undefined ||\n d === undefined ||\n !Number.isFinite(y) ||\n !Number.isFinite(m) ||\n !Number.isFinite(d)\n ) {\n return NaN;\n }\n return Date.UTC(y, m - 1, d);\n}\n\nfunction parseNumber(value: unknown): number {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return value;\n }\n if (typeof value === 'string' && value.trim() !== '') {\n const n = Number(value);\n return Number.isFinite(n) ? n : 0;\n }\n return 0;\n}\n\nfunction normalizeDateBucket(timestamp: string): string {\n return timestamp.slice(0, 10);\n}\n\ninterface InstallBucket {\n date: string;\n channel: string | null;\n campaign: string | null;\n installs: number;\n opens: number;\n conversions: number;\n costEstimated: number;\n}\n\nfunction bucketKey(\n date: string,\n channel: string | null,\n campaign: string | null,\n): string {\n return `${date}|${channel ?? ''}|${campaign ?? ''}`;\n}\n\nexport function mergeInstallBuckets(\n rowsByDataSource: Record<InstallDataSource, BranchInstallResultRow[]>,\n): InstallBucket[] {\n const buckets = new Map<string, InstallBucket>();\n for (const dataSource of INSTALL_DATA_SOURCES) {\n const field = COUNT_FIELD_BY_DATA_SOURCE[dataSource];\n for (const row of rowsByDataSource[dataSource]) {\n const date = normalizeDateBucket(row.result.timestamp);\n const channel =\n (row.result[CHANNEL_DIMENSION] as string | null | undefined) ?? null;\n const campaign =\n (row.result[CAMPAIGN_DIMENSION] as string | null | undefined) ?? null;\n const key = bucketKey(date, channel, campaign);\n let bucket = buckets.get(key);\n if (!bucket) {\n bucket = {\n date,\n channel,\n campaign,\n installs: 0,\n opens: 0,\n conversions: 0,\n costEstimated: 0,\n };\n buckets.set(key, bucket);\n }\n const count = parseNumber(row.unique_count);\n if (field === 'installs') {\n bucket.installs += count;\n } else if (field === 'opens') {\n bucket.opens += count;\n } else {\n bucket.conversions += count;\n }\n if (dataSource === 'eo_install') {\n bucket.costEstimated += parseNumber(row.result.cost_in_local_currency);\n }\n }\n }\n return Array.from(buckets.values()).sort((a, b) =>\n a.date < b.date ? -1 : a.date > b.date ? 1 : 0,\n );\n}\n\nexport function installBucketToMetricSample(\n bucket: InstallBucket,\n): MetricSample {\n const ts = isoDateToMs(bucket.date);\n return {\n name: INSTALL_METRIC_NAME,\n ts: Number.isFinite(ts) ? ts : 0,\n value: bucket.installs,\n attributes: {\n date: bucket.date,\n channel: bucket.channel,\n campaign: bucket.campaign,\n installs: bucket.installs,\n opens: bucket.opens,\n conversions: bucket.conversions,\n costEstimated: bucket.costEstimated,\n },\n };\n}\n\nexport function clickRowToEventRecord(row: BranchClickResultRow): Event {\n const date = normalizeDateBucket(row.result.timestamp);\n const channel =\n (row.result[CHANNEL_DIMENSION] as string | null | undefined) ?? null;\n const campaign =\n (row.result[CAMPAIGN_DIMENSION] as string | null | undefined) ?? null;\n const feature =\n (row.result[FEATURE_DIMENSION] as string | null | undefined) ?? null;\n const ts = isoDateToMs(date);\n const clicks = parseNumber(row.unique_count);\n const startTs = Number.isFinite(ts) ? ts : 0;\n return {\n name: DEEP_LINK_EVENT_NAME,\n start_ts: startTs,\n end_ts: startTs,\n attributes: {\n bucketKey: `${date}|${channel ?? ''}|${campaign ?? ''}|${feature ?? ''}`,\n date,\n channel,\n campaign,\n feature,\n clicks,\n },\n };\n}\n\nexport const id = 'branch';\n\nexport class BranchConnector extends BaseConnector<\n BranchSettings,\n BranchCredentials\n> {\n static readonly id = id;\n\n static readonly resources = branchResources;\n\n static readonly schemas = schemasFromResources(branchResources);\n\n static create(input: unknown, ctx?: ConnectorContext): BranchConnector {\n const parsed = configFields.parse(input);\n return new BranchConnector(\n { lookbackDays: parsed.lookbackDays, resources: parsed.resources },\n { branchKey: parsed.branchKey, branchSecret: parsed.branchSecret },\n ctx,\n );\n }\n\n readonly id = id;\n override readonly credentials = branchCredentials;\n\n private buildHeaders(): Record<string, string> {\n return {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n 'User-Agent': connectorUserAgent('branch'),\n };\n }\n\n private buildBody(\n dataSource: string,\n dimensions: string[],\n window: BranchWindow,\n ): string {\n return JSON.stringify({\n branch_key: this.creds.branchKey,\n branch_secret: this.creds.branchSecret,\n start_date: window.from,\n end_date: window.to,\n data_source: dataSource,\n dimensions,\n granularity: 'day',\n aggregation: 'unique_count',\n ordered: 'ascending',\n ordered_by: 'timestamp',\n });\n }\n\n private async fetchAggregate<T>(\n resource: string,\n dataSource: string,\n dimensions: string[],\n window: BranchWindow,\n signal?: AbortSignal,\n ): Promise<{ results?: T[] }> {\n const res = await this.post<{ results?: T[] }>(ANALYTICS_API_URL, {\n resource,\n headers: this.buildHeaders(),\n body: this.buildBody(dataSource, dimensions, window),\n signal,\n });\n return res.body;\n }\n\n private async fetchInstallBuckets(\n window: BranchWindow,\n signal?: AbortSignal,\n ): Promise<InstallBucket[]> {\n const dims = [CHANNEL_DIMENSION, CAMPAIGN_DIMENSION];\n const rowsByDataSource = {\n eo_install: [] as BranchInstallResultRow[],\n eo_open: [] as BranchInstallResultRow[],\n eo_event: [] as BranchInstallResultRow[],\n };\n for (const dataSource of INSTALL_DATA_SOURCES) {\n const field = COUNT_FIELD_BY_DATA_SOURCE[dataSource];\n const tag = `install_metrics_${field}`;\n const body = await this.fetchAggregate<BranchInstallResultRow>(\n tag,\n dataSource,\n dims,\n window,\n signal,\n );\n rowsByDataSource[dataSource] = body.results ?? [];\n }\n return mergeInstallBuckets(rowsByDataSource);\n }\n\n private async fetchClickRows(\n window: BranchWindow,\n signal?: AbortSignal,\n ): Promise<BranchClickResultRow[]> {\n const body = await this.fetchAggregate<BranchClickResultRow>(\n 'deep_link_events',\n 'eo_click',\n [CHANNEL_DIMENSION, CAMPAIGN_DIMENSION, FEATURE_DIMENSION],\n window,\n signal,\n );\n return body.results ?? [];\n }\n\n private async writePhase(\n storage: StorageHandle,\n phase: BranchPhase,\n window: BranchWindow,\n signal?: AbortSignal,\n ): Promise<void> {\n if (phase === 'install_metrics') {\n const buckets = await this.fetchInstallBuckets(window, signal);\n await storage.metrics([], { names: [INSTALL_METRIC_NAME] });\n for (const bucket of buckets) {\n await storage.metric(installBucketToMetricSample(bucket));\n }\n return;\n }\n const rows = await this.fetchClickRows(window, signal);\n for (const row of rows) {\n await storage.event(clickRowToEventRecord(row));\n }\n }\n\n async sync(\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ): Promise<SyncResult> {\n const cursor: BranchSyncCursor | undefined = isBranchSyncCursor(\n options.cursor,\n )\n ? options.cursor\n : undefined;\n const lookbackDays = this.settings.lookbackDays ?? DEFAULT_LOOKBACK_DAYS;\n const window = getWindow(options, lookbackDays);\n\n const phases = selectActivePhases<BranchResource, BranchPhase>(\n (r) => r,\n PHASE_ORDER,\n this.settings.resources,\n );\n\n return paginateChunked<BranchPhase, string>({\n phases,\n cursor,\n signal,\n logger: this.logger,\n fetchPage: async (_phase, _page, _sig) => ({ items: [null], next: null }),\n writeBatch: async (phase, _items, _page) => {\n await this.writePhase(storage, phase, window, signal);\n },\n });\n }\n}\n","import { BranchConnector } from './branch';\n\nexport {\n BranchConnector,\n branchResources as resources,\n clickRowToEventRecord,\n configFields,\n doc,\n getWindow,\n id,\n installBucketToMetricSample,\n mergeInstallBuckets,\n} from './branch';\nexport type {\n BranchClickResultRow,\n BranchInstallResultRow,\n BranchResource,\n BranchSettings,\n} from './branch';\nexport default BranchConnector;\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../connector-shared/src/errors.ts","../../../connector-shared/src/retry.ts","../../../connector-shared/src/version.ts","../../../connector-shared/src/request.ts","../../../connector-shared/src/rate-limit.ts","../../../connector-shared/src/map-concurrent.ts","../../../connector-shared/src/sanitize.ts","../../../connector-shared/src/epoch.ts","../../../connector-shared/src/pagination.ts","../../../connector-shared/src/logger.ts","../src/branch.ts","../src/index.ts"],"sourcesContent":["import type { HttpResponse } from './types';\n\nexport type HttpErrorKind =\n | 'transient'\n | 'rate_limit'\n | 'auth'\n | 'upstream_bug'\n | 'client_bug';\n\nexport abstract class HttpClientError extends Error {\n abstract readonly kind: HttpErrorKind;\n readonly response?: HttpResponse;\n\n constructor(message: string, response?: HttpResponse) {\n super(message);\n this.name = new.target.name;\n this.response = response;\n }\n}\n\nexport class TransientError extends HttpClientError {\n readonly kind = 'transient' as const;\n}\n\nexport class RateLimitError extends HttpClientError {\n readonly kind = 'rate_limit' as const;\n readonly retryAfter?: Date;\n\n constructor(message: string, response?: HttpResponse, retryAfter?: Date) {\n super(message, response);\n this.retryAfter = retryAfter;\n }\n}\n\nexport class AuthError extends HttpClientError {\n readonly kind = 'auth' as const;\n}\n\nexport class UpstreamBugError extends HttpClientError {\n readonly kind = 'upstream_bug' as const;\n}\n\nexport class ClientBugError extends HttpClientError {\n readonly kind = 'client_bug' as const;\n}\n\nexport function classifyStatus(status: number): HttpErrorKind {\n if (status === 429) {\n return 'rate_limit';\n }\n if (status === 401 || status === 403) {\n return 'auth';\n }\n if (status === 408) {\n return 'transient';\n }\n if (status >= 500) {\n return 'upstream_bug';\n }\n if (status >= 400) {\n return 'client_bug';\n }\n return 'client_bug';\n}\n\nexport function errorForStatus(\n message: string,\n response: HttpResponse,\n retryAfter?: Date,\n): HttpClientError {\n const kind = classifyStatus(response.status);\n switch (kind) {\n case 'rate_limit':\n return new RateLimitError(message, response, retryAfter);\n case 'auth':\n return new AuthError(message, response);\n case 'transient':\n return new TransientError(message, response);\n case 'upstream_bug':\n return new UpstreamBugError(message, response);\n case 'client_bug':\n return new ClientBugError(message, response);\n }\n}\n","import { HttpClientError, RateLimitError, TransientError } from './errors';\n\nexport interface RetryPolicy {\n maxAttempts?: number;\n initialDelayMs?: number;\n maxDelayMs?: number;\n retryOn?: (status: number | null, err?: Error) => boolean;\n}\n\nexport const defaultRetryOn = (status: number | null, err?: Error): boolean => {\n if (err instanceof RateLimitError) {\n return true;\n }\n if (err instanceof TransientError) {\n return true;\n }\n if (status === null) {\n return err instanceof Error && !(err instanceof HttpClientError);\n }\n if (status === 408 || status === 429) {\n return true;\n }\n if (status >= 500) {\n return true;\n }\n return false;\n};\n\nexport function backoffDelayMs(\n attempt: number,\n policy: Required<Pick<RetryPolicy, 'initialDelayMs' | 'maxDelayMs'>>,\n): number {\n const base = policy.initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, policy.maxDelayMs);\n}\n\nexport function parseRetryAfter(\n headerValue: string | null,\n now: Date = new Date(),\n): Date | undefined {\n if (!headerValue) {\n return undefined;\n }\n const trimmed = headerValue.trim();\n if (/^\\d+$/.test(trimmed)) {\n return new Date(now.getTime() + Number(trimmed) * 1000);\n }\n const parsed = Date.parse(trimmed);\n if (Number.isNaN(parsed)) {\n return undefined;\n }\n return new Date(parsed);\n}\n\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) {\n return Promise.reject(signal.reason ?? new Error('Aborted'));\n }\n return new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal!.reason ?? new Error('Aborted'));\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n","export const HTTP_CLIENT_VERSION = '0.0.0';\n\nexport const DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n\nexport function connectorUserAgent(connectorId: string): string {\n return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n}\n","import {\n AuthError,\n ClientBugError,\n HttpClientError,\n RateLimitError,\n TransientError,\n UpstreamBugError,\n errorForStatus,\n} from './errors';\nimport { defaultRetryOn, parseRetryAfter, sleep } from './retry';\nimport type { FetchLike, HttpMethod, HttpRequest, HttpResponse } from './types';\nimport { DEFAULT_USER_AGENT } from './version';\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_ATTEMPTS = 3;\nconst DEFAULT_INITIAL_DELAY_MS = 1000;\nconst DEFAULT_MAX_DELAY_MS = 60_000;\nconst OBSERVER_TIMEOUT_MS = 250;\n\nexport interface RequestObservation {\n url: string;\n method: HttpMethod;\n status: number;\n resource: string;\n requestId: string;\n body: unknown;\n}\n\nexport type RequestObserver = (\n event: RequestObservation,\n) => void | Promise<void>;\n\nexport interface RequestOptions {\n fetch?: FetchLike;\n observer?: RequestObserver;\n resource: string;\n requestId?: string;\n}\n\nasync function notifyObserver(\n observer: RequestObserver,\n event: RequestObservation,\n): Promise<void> {\n let result: void | Promise<void>;\n try {\n result = observer(event);\n } catch (err) {\n console.warn('[connector-shared] request observer threw:', err);\n return;\n }\n if (!(result instanceof Promise)) {\n return;\n }\n const guarded = result.catch((err) => {\n console.warn('[connector-shared] request observer rejected:', err);\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, OBSERVER_TIMEOUT_MS);\n });\n try {\n await Promise.race([guarded, timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\nfunction newRequestId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) {\n return c.randomUUID();\n }\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction mergeHeaders(\n defaults: Record<string, string>,\n overrides: Record<string, string> | undefined,\n): Record<string, string> {\n const merged: Record<string, string> = {};\n for (const [k, v] of Object.entries(defaults)) {\n merged[k.toLowerCase()] = v;\n }\n if (overrides) {\n for (const [k, v] of Object.entries(overrides)) {\n merged[k.toLowerCase()] = v;\n }\n }\n return merged;\n}\n\nfunction linkTimeoutSignal(\n parent: AbortSignal | undefined,\n timeoutMs: number,\n): { signal: AbortSignal; cancel: () => void } {\n const controller = new AbortController();\n const onParentAbort = () => {\n controller.abort(parent?.reason);\n };\n if (parent) {\n if (parent.aborted) {\n controller.abort(parent.reason);\n } else {\n parent.addEventListener('abort', onParentAbort, { once: true });\n }\n }\n const timer = setTimeout(() => {\n controller.abort(new Error(`Request timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n return {\n signal: controller.signal,\n cancel: () => {\n clearTimeout(timer);\n if (parent) {\n parent.removeEventListener('abort', onParentAbort);\n }\n },\n };\n}\n\nasync function readBody(res: Response, parseJson: boolean): Promise<unknown> {\n if (res.status === 204 || res.status === 205) {\n return null;\n }\n const contentType = res.headers.get('content-type') ?? '';\n if (parseJson && contentType.includes('application/json')) {\n const text = await res.text();\n if (text.length === 0) {\n return null;\n }\n return JSON.parse(text);\n }\n return res.text();\n}\n\nexport async function request<T = unknown>(\n req: HttpRequest,\n options: RequestOptions,\n): Promise<HttpResponse<T>> {\n const fetchImpl: FetchLike = options.fetch ?? (globalThis.fetch as FetchLike);\n const retry = req.retry ?? {};\n const maxAttempts = retry.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const initialDelayMs = retry.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;\n const maxDelayMs = retry.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;\n const retryOn = retry.retryOn ?? defaultRetryOn;\n const timeoutMs = req.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const parseJson = req.parseJson ?? true;\n\n const headers = mergeHeaders(\n {\n 'User-Agent': DEFAULT_USER_AGENT,\n Accept: 'application/json',\n },\n req.headers,\n );\n\n let lastErr: Error | undefined;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n req.signal?.throwIfAborted();\n\n const { signal, cancel } = linkTimeoutSignal(req.signal, timeoutMs);\n let res: Response;\n try {\n res = await fetchImpl(req.url, {\n method: req.method ?? 'GET',\n headers,\n body: req.body as RequestInit['body'],\n signal,\n });\n } catch (err) {\n cancel();\n if (req.signal?.aborted) {\n throw req.signal.reason ?? err;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n lastErr = error;\n if (attempt < maxAttempts - 1 && retryOn(null, error)) {\n const delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n await sleep(delay, req.signal);\n continue;\n }\n throw new TransientError(error.message);\n }\n cancel();\n\n const body = await readBody(res, parseJson);\n const httpResponse: HttpResponse<T> = {\n status: res.status,\n headers: res.headers,\n body: body as T,\n };\n if (req.rateLimit) {\n const state = req.rateLimit.parse(res.headers);\n if (state) {\n httpResponse.rateLimitState = state;\n }\n }\n\n if (options.observer) {\n await notifyObserver(options.observer, {\n url: req.url,\n method: req.method ?? 'GET',\n status: res.status,\n resource: options.resource,\n requestId: options.requestId ?? newRequestId(),\n body,\n });\n }\n\n if (res.ok) {\n return httpResponse;\n }\n\n const retryAfter = parseRetryAfter(res.headers.get('retry-after'));\n const message = `HTTP ${res.status} ${res.statusText} for ${req.method ?? 'GET'} ${req.url}`;\n const err = errorForStatus(message, httpResponse, retryAfter);\n\n if (\n attempt < maxAttempts - 1 &&\n retryOn(res.status, err) &&\n !(err instanceof AuthError) &&\n !(err instanceof ClientBugError)\n ) {\n lastErr = err;\n let delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n if (err instanceof RateLimitError && retryAfter) {\n const wait = retryAfter.getTime() - Date.now();\n if (wait > 0) {\n delay = Math.min(wait, maxDelayMs);\n }\n }\n await sleep(delay, req.signal);\n continue;\n }\n\n throw err;\n }\n\n throw lastErr ?? new UpstreamBugError('Exhausted retry attempts');\n}\n\nfunction computeDelay(\n attempt: number,\n initialDelayMs: number,\n maxDelayMs: number,\n): number {\n const base = initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, maxDelayMs);\n}\n\nexport { HttpClientError };\n","export interface RateLimitState {\n remaining: number;\n resetAt: Date;\n}\n\nexport interface RateLimitPolicy {\n parse(headers: Headers): RateLimitState | null;\n}\n\nexport interface StandardRateLimitPolicyConfig {\n remainingHeader: string;\n resetHeader: string;\n resetUnit: 's' | 'ms';\n resetFallbackMs?: number;\n}\n\nexport function standardRateLimitPolicy(\n config: StandardRateLimitPolicyConfig,\n): RateLimitPolicy {\n const { remainingHeader, resetHeader, resetUnit, resetFallbackMs } = config;\n const multiplier = resetUnit === 's' ? 1000 : 1;\n return {\n parse(h) {\n const remainingRaw = h.get(remainingHeader);\n if (remainingRaw === null || remainingRaw.trim() === '') {\n return null;\n }\n const remaining = Number(remainingRaw);\n if (!Number.isFinite(remaining)) {\n return null;\n }\n const resetRaw = h.get(resetHeader);\n if (resetRaw === null) {\n if (resetFallbackMs === undefined) {\n return null;\n }\n return {\n remaining,\n resetAt: new Date(Date.now() + resetFallbackMs),\n };\n }\n if (resetRaw.trim() === '') {\n return null;\n }\n const reset = Number(resetRaw);\n if (!Number.isFinite(reset) || reset < 0) {\n return null;\n }\n const resetMs = reset * multiplier;\n if (!Number.isFinite(resetMs)) {\n return null;\n }\n return { remaining, resetAt: new Date(resetMs) };\n },\n };\n}\n","export async function mapWithConcurrency<T, R>(\n items: readonly T[],\n concurrency: number,\n fn: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n const results = new Array<R>(items.length);\n if (items.length === 0) {\n return results;\n }\n const normalized = Number.isFinite(concurrency) ? Math.floor(concurrency) : 1;\n const limit = Math.max(1, Math.min(normalized, items.length));\n let next = 0;\n let failed = false;\n\n async function worker(): Promise<void> {\n while (!failed) {\n const i = next++;\n if (i >= items.length) {\n return;\n }\n try {\n results[i] = await fn(items[i]!, i);\n } catch (err) {\n failed = true;\n throw err;\n }\n }\n }\n\n const workers: Promise<void>[] = [];\n for (let w = 0; w < limit; w++) {\n workers.push(worker());\n }\n await Promise.all(workers);\n return results;\n}\n","export interface SanitizeAllowedUrlOptions {\n url: string | null;\n host: string;\n pathname: string;\n protocol?: 'https:' | 'http:';\n}\n\nexport function sanitizeAllowedUrl(\n options: SanitizeAllowedUrlOptions,\n): string | null {\n const { url, host, pathname, protocol = 'https:' } = options;\n if (url === null) {\n return null;\n }\n try {\n const u = new URL(url);\n if (u.protocol !== protocol || u.host !== host || u.pathname !== pathname) {\n return null;\n }\n return u.toString();\n } catch {\n return null;\n }\n}\n","export type EpochUnit = 'ms' | 's' | 'iso';\n\nexport function parseEpoch(\n value: number | string | null | undefined,\n unit: EpochUnit,\n): number | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (unit === 'iso') {\n if (typeof value !== 'string') {\n return null;\n }\n const ms = new Date(value).getTime();\n return Number.isFinite(ms) ? ms : null;\n }\n if (typeof value === 'string' && value.trim() === '') {\n return null;\n }\n const n = typeof value === 'number' ? value : Number(value);\n if (!Number.isFinite(n)) {\n return null;\n }\n const result = unit === 's' ? n * 1000 : n;\n return Number.isFinite(result) ? result : null;\n}\n","import { request } from './request';\nimport type { HttpRequest } from './types';\n\nexport function parseLinkHeader(header: string | null): Record<string, string> {\n if (!header) {\n return {};\n }\n const result: Record<string, string> = {};\n for (const part of header.split(',')) {\n const match = part.match(/<([^>]+)>\\s*;\\s*rel=\"([^\"]+)\"/);\n if (match) {\n result[match[2]!] = match[1]!;\n }\n }\n return result;\n}\n\nexport async function* paginateLink<T>(\n initial: HttpRequest,\n parse: (body: unknown) => T[],\n options: { resource: string },\n): AsyncIterable<T> {\n let next: string | null = initial.url;\n while (next) {\n const res: Awaited<ReturnType<typeof request>> = await request(\n {\n ...initial,\n url: next,\n },\n { resource: options.resource },\n );\n for (const item of parse(res.body)) {\n yield item;\n }\n const links = parseLinkHeader(res.headers.get('link'));\n next = links['next'] ?? null;\n }\n}\n\nexport async function* paginateCursor<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; nextCursor: string | null },\n buildNext: (req: HttpRequest, cursor: string) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let req: HttpRequest = initial;\n while (true) {\n const res = await request(req, { resource: options.resource });\n const { items, nextCursor } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!nextCursor) {\n return;\n }\n req = buildNext(req, nextCursor);\n }\n}\n\nexport async function* paginatePage<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; hasMore: boolean },\n buildPage: (req: HttpRequest, page: number) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let page = 1;\n while (true) {\n const req = page === 1 ? initial : buildPage(initial, page);\n const res = await request(req, { resource: options.resource });\n const { items, hasMore } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!hasMore || items.length === 0) {\n return;\n }\n page++;\n }\n}\n","export type LogFields = Record<string, unknown>;\n\nexport interface ConnectorLogger {\n info(event: string, fields?: LogFields): void;\n warn(event: string, fields?: LogFields): void;\n}\n\nexport interface ConnectorLoggerOptions {\n scope: string;\n}\n\nconst MAX_VALUE_LEN = 120;\n\nfunction truncate(s: string, max = MAX_VALUE_LEN): string {\n if (s.length <= max) {\n return s;\n }\n return `${s.slice(0, max - 1)}…`;\n}\n\nfunction formatValue(value: unknown): string {\n if (value === null) {\n return 'null';\n }\n if (value === undefined) {\n return '';\n }\n if (typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n if (typeof value === 'string') {\n const t = truncate(value);\n if (/[\\s\"=]/.test(t)) {\n return JSON.stringify(t);\n }\n return t;\n }\n if (typeof value === 'bigint') {\n return value.toString();\n }\n let json: string | undefined;\n try {\n json = JSON.stringify(value);\n } catch {\n json = undefined;\n }\n return truncate(json ?? String(value));\n}\n\nexport function formatLogFields(fields?: LogFields): string {\n if (!fields) {\n return '';\n }\n const parts: string[] = [];\n for (const [k, v] of Object.entries(fields)) {\n if (v === undefined) {\n continue;\n }\n parts.push(`${k}=${formatValue(v)}`);\n }\n return parts.length > 0 ? ` ${parts.join(' ')}` : '';\n}\n\nexport function formatLogLine(\n scope: string,\n event: string,\n fields?: LogFields,\n): string {\n return `[${scope}] ${event}${formatLogFields(fields)}`;\n}\n\nexport function createDefaultConnectorLogger(\n opts: ConnectorLoggerOptions,\n): ConnectorLogger {\n return {\n info(event, fields) {\n console.info(formatLogLine(opts.scope, event, fields));\n },\n warn(event, fields) {\n console.warn(formatLogLine(opts.scope, event, fields));\n },\n };\n}\n\nconst NOOP_LOGGER: ConnectorLogger = {\n info() {},\n warn() {},\n};\n\nexport function noopConnectorLogger(): ConnectorLogger {\n return NOOP_LOGGER;\n}\n","import { connectorUserAgent } from '@rawdash/connector-shared';\nimport {\n BaseConnector,\n type ChunkedSyncCursor,\n type ConnectorContext,\n type ConnectorDoc,\n type CredentialsSchema,\n type Event,\n type MetricSample,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\n defineConnectorDoc,\n defineResources,\n makeChunkedCursorGuard,\n paginateChunked,\n schemasFromResources,\n selectActivePhases,\n} from '@rawdash/core';\nimport { z } from 'zod';\n\nexport const configFields = defineConfigFields(\n z.object({\n branchKey: z.object({ $secret: z.string() }).meta({\n label: 'Branch key',\n description:\n 'Your Branch app key (starts with `key_live_`). Find it in the Branch dashboard under Account Settings -> Profile.',\n placeholder: 'key_live_xxxxxxxxxxxxxxxxxxxxxxxxxx',\n secret: true,\n }),\n branchSecret: z.object({ $secret: z.string() }).meta({\n label: 'Branch secret',\n description:\n 'Your Branch app secret (starts with `secret_live_`). Find it next to the key in the Branch dashboard.',\n placeholder: 'secret_live_xxxxxxxxxxxxxxxxxxxxxxxxxx',\n secret: true,\n }),\n lookbackDays: z.number().int().positive().optional().meta({\n label: 'Lookback days (full sync)',\n description:\n 'How many calendar days of metrics/events to fetch on a full sync. Defaults to 90.',\n placeholder: '90',\n }),\n resources: z\n .array(z.enum(['install_metrics', 'deep_link_events']))\n .nonempty()\n .optional()\n .meta({\n label: 'Resources',\n description:\n 'Which Branch resources to sync. Omit to sync all of them.',\n }),\n }),\n);\n\nexport const doc: ConnectorDoc = defineConnectorDoc({\n displayName: 'Branch',\n category: 'marketing',\n brandColor: '#7CB833',\n tagline:\n 'Sync Branch install attribution metrics (installs, opens, conversions) and deep-link click events from the Query API for mobile attribution dashboards.',\n vendor: {\n name: 'Branch',\n domain: 'branch.io',\n apiDocs: 'https://help.branch.io/developers-hub/reference',\n website: 'https://www.branch.io',\n },\n auth: {\n summary:\n 'A Branch app key and secret, sent together in the Query API request body to authenticate each call.',\n setup: [\n 'In the Branch dashboard, open Account Settings -> Profile and copy the Branch Key (starts with `key_live_`).',\n 'On the same screen, reveal and copy the Branch Secret (starts with `secret_live_`). Both values are app-scoped; keep them in a secret store.',\n 'Reference them from the connector config as `branchKey: secret(\"BRANCH_KEY\")` and `branchSecret: secret(\"BRANCH_SECRET\")`.',\n ],\n },\n rateLimit:\n 'The Branch Query API allows roughly 5 requests/second, 20/minute, and 150/hour per app. Because each sync splits its window into <=7-day segments and paginates, a wide window fans out to many requests; the connector relies on the shared HTTP client to honor 429 responses and the `Retry-After` header with backoff.',\n limitations: [\n 'Daily granularity only - the connector requests `granularity=day` from the Branch Query API to keep result cardinality bounded.',\n 'Branch rejects windows wider than 7 days, so each requested range is split into <=7-day segments and fetched one segment at a time.',\n 'Deep-link events are aggregated daily click counts per (date, channel, campaign, feature). Individual click-level records require the Branch Daily Export API which is intentionally out of scope.',\n ],\n});\n\nexport interface BranchSettings {\n lookbackDays?: number;\n resources?: readonly BranchResource[];\n}\n\nconst branchCredentials = {\n branchKey: {\n description: 'Branch app key (key_live_...)',\n auth: 'required' as const,\n },\n branchSecret: {\n description: 'Branch app secret (secret_live_...)',\n auth: 'required' as const,\n },\n} satisfies CredentialsSchema;\n\ntype BranchCredentials = typeof branchCredentials;\n\nconst PHASE_ORDER = ['install_metrics', 'deep_link_events'] as const;\n\ntype BranchPhase = (typeof PHASE_ORDER)[number];\n\nexport type BranchResource = BranchPhase;\n\ntype BranchSyncCursor = ChunkedSyncCursor<BranchPhase, string>;\n\nconst isBranchSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);\n\nconst ANALYTICS_API_URL = 'https://api2.branch.io/v1/query/analytics';\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\nconst DEFAULT_LOOKBACK_DAYS = 90;\nconst INCREMENTAL_LOOKBACK_DAYS = 14;\nconst MAX_WINDOW_DAYS = 7;\nconst PAGE_LIMIT = 1000;\n\nconst INSTALL_METRIC_NAME = 'branch_install_metrics';\nconst DEEP_LINK_EVENT_NAME = 'branch_deep_link_event';\n\nconst CHANNEL_DIMENSION = 'last_attributed_touch_data_tilde_channel';\nconst CAMPAIGN_DIMENSION = 'last_attributed_touch_data_tilde_campaign';\nconst FEATURE_DIMENSION = 'last_attributed_touch_data_tilde_feature';\n\nconst INSTALL_DATA_SOURCES = [\n 'eo_install',\n 'eo_open',\n 'eo_custom_event',\n] as const;\ntype InstallDataSource = (typeof INSTALL_DATA_SOURCES)[number];\n\nconst COUNT_FIELD_BY_DATA_SOURCE: Record<InstallDataSource, string> = {\n eo_install: 'installs',\n eo_open: 'opens',\n eo_custom_event: 'conversions',\n};\n\nconst isoTimestampString = z\n .string()\n .regex(\n /^\\d{4}-\\d{2}-\\d{2}(?:[T ]\\d{2}:\\d{2}(?::\\d{2})?(?:\\.\\d+)?(?:Z|[+-]\\d{2}:?\\d{2})?)?$/,\n );\nconst numericLike = z.union([z.number(), z.string(), z.null()]).optional();\nconst pagingSchema = z.object({ next_url: z.string().nullish() }).nullish();\n\nconst installResultRowSchema = z.object({\n timestamp: isoTimestampString,\n result: z.object({\n [CHANNEL_DIMENSION]: z.string().nullish(),\n [CAMPAIGN_DIMENSION]: z.string().nullish(),\n unique_count: numericLike,\n }),\n});\n\nconst installResponseSchema = z.object({\n results: z.array(installResultRowSchema),\n paging: pagingSchema,\n});\n\nconst clickResultRowSchema = z.object({\n timestamp: isoTimestampString,\n result: z.object({\n [CHANNEL_DIMENSION]: z.string().nullish(),\n [CAMPAIGN_DIMENSION]: z.string().nullish(),\n [FEATURE_DIMENSION]: z.string().nullish(),\n unique_count: numericLike,\n }),\n});\n\nconst clickResponseSchema = z.object({\n results: z.array(clickResultRowSchema),\n paging: pagingSchema,\n});\n\nexport const branchResources = defineResources({\n [INSTALL_METRIC_NAME]: {\n shape: 'metric',\n description:\n 'Daily Branch attribution metrics bucketed by channel and campaign. Primary value is `installs`; `opens` and `conversions` are carried as attributes.',\n endpoint: 'POST /v1/query/analytics',\n unit: 'installs',\n granularity: 'day',\n notes:\n 'Merges three Query API calls (data_source=eo_install, eo_open, eo_custom_event) keyed by (date, channel, campaign). Rows with missing channel or campaign are recorded as `null` for that attribute.',\n dimensions: [\n { name: 'date', description: 'Calendar day of the metric sample (UTC).' },\n { name: 'channel', description: 'Branch last-attributed channel.' },\n { name: 'campaign', description: 'Branch last-attributed campaign.' },\n { name: 'installs', description: 'Attributed installs on the day.' },\n { name: 'opens', description: 'Attributed app opens on the day.' },\n {\n name: 'conversions',\n description: 'Attributed in-app custom-event conversions on the day.',\n },\n ],\n responses: {\n install_metrics_installs: installResponseSchema,\n install_metrics_opens: installResponseSchema,\n install_metrics_conversions: installResponseSchema,\n },\n },\n [DEEP_LINK_EVENT_NAME]: {\n shape: 'event',\n description:\n 'Daily aggregated Branch deep-link click events bucketed by channel, campaign, and feature. One event per (date, channel, campaign, feature) row carrying the daily click count.',\n endpoint: 'POST /v1/query/analytics',\n notes:\n 'Sourced from data_source=eo_click. Event id encodes the bucket so resyncs are idempotent.',\n fields: [\n { name: 'date', description: 'Calendar day of the click bucket (UTC).' },\n { name: 'channel', description: 'Branch last-attributed channel.' },\n { name: 'campaign', description: 'Branch last-attributed campaign.' },\n {\n name: 'feature',\n description: 'Branch last-attributed feature (e.g. `sharing`).',\n },\n { name: 'clicks', description: 'Click count for the bucket.' },\n ],\n filterable: [],\n responses: { deep_link_events: clickResponseSchema },\n },\n});\n\nexport type BranchInstallResultRow = z.infer<typeof installResultRowSchema>;\nexport type BranchClickResultRow = z.infer<typeof clickResultRowSchema>;\n\ninterface BranchWindow {\n from: string;\n to: string;\n}\n\nfunction pad2(n: number): string {\n return String(n).padStart(2, '0');\n}\n\nfunction toIsoDate(ms: number): string {\n const d = new Date(ms);\n return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;\n}\n\nfunction startOfUtcDay(ms: number): number {\n return Math.floor(ms / MS_PER_DAY) * MS_PER_DAY;\n}\n\nexport function getWindow(\n options: SyncOptions,\n lookbackDays: number,\n now: number = Date.now(),\n): BranchWindow {\n const today = startOfUtcDay(now);\n if (options.mode === 'latest') {\n return {\n from: toIsoDate(today - (INCREMENTAL_LOOKBACK_DAYS - 1) * MS_PER_DAY),\n to: toIsoDate(today),\n };\n }\n if (options.since) {\n const sinceMs = new Date(options.since).getTime();\n if (Number.isFinite(sinceMs)) {\n const requested = Math.max(\n 1,\n Math.ceil((today - startOfUtcDay(sinceMs)) / MS_PER_DAY) + 1,\n );\n const capped = Math.min(requested, lookbackDays);\n return {\n from: toIsoDate(today - (capped - 1) * MS_PER_DAY),\n to: toIsoDate(today),\n };\n }\n }\n return {\n from: toIsoDate(today - (lookbackDays - 1) * MS_PER_DAY),\n to: toIsoDate(today),\n };\n}\n\nfunction isoDateToMs(date: string): number {\n const [y, m, d] = date.split('-').map((part) => Number(part));\n if (\n y === undefined ||\n m === undefined ||\n d === undefined ||\n !Number.isFinite(y) ||\n !Number.isFinite(m) ||\n !Number.isFinite(d)\n ) {\n return NaN;\n }\n return Date.UTC(y, m - 1, d);\n}\n\nexport function splitWindow(window: BranchWindow): BranchWindow[] {\n const fromMs = isoDateToMs(window.from);\n const toMs = isoDateToMs(window.to);\n if (!Number.isFinite(fromMs) || !Number.isFinite(toMs) || fromMs > toMs) {\n return [window];\n }\n const segments: BranchWindow[] = [];\n let startMs = fromMs;\n while (startMs <= toMs) {\n const endMs = Math.min(startMs + (MAX_WINDOW_DAYS - 1) * MS_PER_DAY, toMs);\n segments.push({ from: toIsoDate(startMs), to: toIsoDate(endMs) });\n startMs = endMs + MS_PER_DAY;\n }\n return segments;\n}\n\nfunction parseNumber(value: unknown): number {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return value;\n }\n if (typeof value === 'string' && value.trim() !== '') {\n const n = Number(value);\n return Number.isFinite(n) ? n : 0;\n }\n return 0;\n}\n\nfunction normalizeDateBucket(timestamp: string): string {\n return timestamp.slice(0, 10);\n}\n\ninterface InstallBucket {\n date: string;\n channel: string | null;\n campaign: string | null;\n installs: number;\n opens: number;\n conversions: number;\n}\n\nfunction bucketKey(\n date: string,\n channel: string | null,\n campaign: string | null,\n): string {\n return `${date}|${channel ?? ''}|${campaign ?? ''}`;\n}\n\nexport function mergeInstallBuckets(\n rowsByDataSource: Record<InstallDataSource, BranchInstallResultRow[]>,\n): InstallBucket[] {\n const buckets = new Map<string, InstallBucket>();\n for (const dataSource of INSTALL_DATA_SOURCES) {\n const field = COUNT_FIELD_BY_DATA_SOURCE[dataSource];\n for (const row of rowsByDataSource[dataSource]) {\n const date = normalizeDateBucket(row.timestamp);\n const channel =\n (row.result[CHANNEL_DIMENSION] as string | null | undefined) ?? null;\n const campaign =\n (row.result[CAMPAIGN_DIMENSION] as string | null | undefined) ?? null;\n const key = bucketKey(date, channel, campaign);\n let bucket = buckets.get(key);\n if (!bucket) {\n bucket = {\n date,\n channel,\n campaign,\n installs: 0,\n opens: 0,\n conversions: 0,\n };\n buckets.set(key, bucket);\n }\n const count = parseNumber(row.result.unique_count);\n if (field === 'installs') {\n bucket.installs += count;\n } else if (field === 'opens') {\n bucket.opens += count;\n } else {\n bucket.conversions += count;\n }\n }\n }\n return Array.from(buckets.values()).sort((a, b) =>\n a.date < b.date ? -1 : a.date > b.date ? 1 : 0,\n );\n}\n\nexport function installBucketToMetricSample(\n bucket: InstallBucket,\n): MetricSample {\n const ts = isoDateToMs(bucket.date);\n return {\n name: INSTALL_METRIC_NAME,\n ts: Number.isFinite(ts) ? ts : 0,\n value: bucket.installs,\n attributes: {\n date: bucket.date,\n channel: bucket.channel,\n campaign: bucket.campaign,\n installs: bucket.installs,\n opens: bucket.opens,\n conversions: bucket.conversions,\n },\n };\n}\n\nexport function clickRowToEventRecord(row: BranchClickResultRow): Event {\n const date = normalizeDateBucket(row.timestamp);\n const channel =\n (row.result[CHANNEL_DIMENSION] as string | null | undefined) ?? null;\n const campaign =\n (row.result[CAMPAIGN_DIMENSION] as string | null | undefined) ?? null;\n const feature =\n (row.result[FEATURE_DIMENSION] as string | null | undefined) ?? null;\n const ts = isoDateToMs(date);\n const clicks = parseNumber(row.result.unique_count);\n const startTs = Number.isFinite(ts) ? ts : 0;\n return {\n name: DEEP_LINK_EVENT_NAME,\n start_ts: startTs,\n end_ts: startTs,\n attributes: {\n bucketKey: `${date}|${channel ?? ''}|${campaign ?? ''}|${feature ?? ''}`,\n date,\n channel,\n campaign,\n feature,\n clicks,\n },\n };\n}\n\nexport const id = 'branch';\n\nexport class BranchConnector extends BaseConnector<\n BranchSettings,\n BranchCredentials\n> {\n static readonly id = id;\n\n static readonly resources = branchResources;\n\n static readonly schemas = schemasFromResources(branchResources);\n\n static create(input: unknown, ctx?: ConnectorContext): BranchConnector {\n const parsed = configFields.parse(input);\n return new BranchConnector(\n { lookbackDays: parsed.lookbackDays, resources: parsed.resources },\n { branchKey: parsed.branchKey, branchSecret: parsed.branchSecret },\n ctx,\n );\n }\n\n readonly id = id;\n override readonly credentials = branchCredentials;\n\n private buildHeaders(): Record<string, string> {\n return {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n 'User-Agent': connectorUserAgent('branch'),\n };\n }\n\n private buildBody(\n dataSource: string,\n dimensions: string[],\n window: BranchWindow,\n ): string {\n return JSON.stringify({\n branch_key: this.creds.branchKey,\n branch_secret: this.creds.branchSecret,\n start_date: window.from,\n end_date: window.to,\n data_source: dataSource,\n dimensions,\n granularity: 'day',\n aggregation: 'unique_count',\n ordered: 'ascending',\n ordered_by: 'timestamp',\n limit: PAGE_LIMIT,\n });\n }\n\n private async fetchAggregate<T>(\n resource: string,\n dataSource: string,\n dimensions: string[],\n window: BranchWindow,\n signal?: AbortSignal,\n ): Promise<T[]> {\n const body = this.buildBody(dataSource, dimensions, window);\n const results: T[] = [];\n const base = new URL(ANALYTICS_API_URL);\n const visited = new Set<string>();\n let url = ANALYTICS_API_URL;\n while (!visited.has(url)) {\n visited.add(url);\n const res = await this.post<{\n results?: T[];\n paging?: { next_url?: string | null } | null;\n }>(url, {\n resource,\n headers: this.buildHeaders(),\n body,\n signal,\n });\n results.push(...(res.body.results ?? []));\n const next = res.body.paging?.next_url;\n if (!next) {\n break;\n }\n const parsedNext = new URL(next, ANALYTICS_API_URL);\n if (\n parsedNext.origin !== base.origin ||\n parsedNext.pathname !== base.pathname\n ) {\n break;\n }\n url = parsedNext.toString();\n }\n return results;\n }\n\n private async fetchInstallBuckets(\n segments: BranchWindow[],\n signal?: AbortSignal,\n ): Promise<InstallBucket[]> {\n const dims = [CHANNEL_DIMENSION, CAMPAIGN_DIMENSION];\n const rowsByDataSource: Record<\n InstallDataSource,\n BranchInstallResultRow[]\n > = {\n eo_install: [],\n eo_open: [],\n eo_custom_event: [],\n };\n for (const segment of segments) {\n for (const dataSource of INSTALL_DATA_SOURCES) {\n const field = COUNT_FIELD_BY_DATA_SOURCE[dataSource];\n const tag = `install_metrics_${field}`;\n const rows = await this.fetchAggregate<BranchInstallResultRow>(\n tag,\n dataSource,\n dims,\n segment,\n signal,\n );\n rowsByDataSource[dataSource].push(...rows);\n }\n }\n return mergeInstallBuckets(rowsByDataSource);\n }\n\n private async fetchClickRows(\n segments: BranchWindow[],\n signal?: AbortSignal,\n ): Promise<BranchClickResultRow[]> {\n const rows: BranchClickResultRow[] = [];\n for (const segment of segments) {\n const segmentRows = await this.fetchAggregate<BranchClickResultRow>(\n 'deep_link_events',\n 'eo_click',\n [CHANNEL_DIMENSION, CAMPAIGN_DIMENSION, FEATURE_DIMENSION],\n segment,\n signal,\n );\n rows.push(...segmentRows);\n }\n return rows;\n }\n\n private async writePhase(\n storage: StorageHandle,\n phase: BranchPhase,\n window: BranchWindow,\n signal?: AbortSignal,\n ): Promise<void> {\n const segments = splitWindow(window);\n if (phase === 'install_metrics') {\n const buckets = await this.fetchInstallBuckets(segments, signal);\n await storage.metrics([], { names: [INSTALL_METRIC_NAME] });\n for (const bucket of buckets) {\n await storage.metric(installBucketToMetricSample(bucket));\n }\n return;\n }\n const rows = await this.fetchClickRows(segments, signal);\n for (const row of rows) {\n await storage.event(clickRowToEventRecord(row));\n }\n }\n\n async sync(\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ): Promise<SyncResult> {\n const cursor: BranchSyncCursor | undefined = isBranchSyncCursor(\n options.cursor,\n )\n ? options.cursor\n : undefined;\n const lookbackDays = this.settings.lookbackDays ?? DEFAULT_LOOKBACK_DAYS;\n const window = getWindow(options, lookbackDays);\n\n const phases = selectActivePhases<BranchResource, BranchPhase>(\n (r) => r,\n PHASE_ORDER,\n this.settings.resources,\n );\n\n return paginateChunked<BranchPhase, string>({\n phases,\n cursor,\n signal,\n logger: this.logger,\n fetchPage: async (_phase, _page, _sig) => ({ items: [null], next: null }),\n writeBatch: async (phase, _items, _page) => {\n await this.writePhase(storage, phase, window, signal);\n },\n });\n }\n}\n","import { BranchConnector } from './branch';\n\nexport {\n BranchConnector,\n branchResources as resources,\n clickRowToEventRecord,\n configFields,\n doc,\n getWindow,\n id,\n installBucketToMetricSample,\n mergeInstallBuckets,\n} from './branch';\nexport type {\n BranchClickResultRow,\n BranchInstallResultRow,\n BranchResource,\n BranchSettings,\n} from './branch';\nexport default BranchConnector;\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rawdash/connector-branch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Rawdash connector for Branch — syncs daily install/open/conversion attribution metrics and deep-link click events via the Branch Cross-Platform Analytics API",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"zod": "^4.4.3",
|
|
27
|
-
"@rawdash/core": "0.
|
|
27
|
+
"@rawdash/core": "0.27.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"fast-check": "^4.8.0",
|