@rawdash/connector-branch 0.24.0 → 0.26.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 CHANGED
@@ -5,7 +5,7 @@
5
5
  [![npm version](https://img.shields.io/npm/v/@rawdash/connector-branch)](https://www.npmjs.com/package/@rawdash/connector-branch)
6
6
  [![license](https://img.shields.io/npm/l/@rawdash/connector-branch)](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 Cross-Platform Analytics API for mobile attribution dashboards.
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, used together to authenticate Cross-Platform Analytics API requests.
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`, `conversions`, and `costEstimated` are carried as attributes.
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`, `costEstimated`
40
- - 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.
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,17 +104,17 @@ export default defineConfig({
104
104
 
105
105
  ## Rate limits
106
106
 
107
- 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.
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 Aggregate API to keep result cardinality bounded.
112
- - Cost attribution is best-effort - Branch only exposes `cost_in_local_currency` for ad-network-integrated channels. Rows without cost data carry `costEstimated: 0`.
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
116
116
 
117
- - [Rawdash docs](https://rawdash.dev/docs/connectors/)
117
+ - [Rawdash docs](https://rawdash.dev/docs/connectors)
118
118
  - [Branch API docs](https://help.branch.io/developers-hub/reference)
119
119
  - [GitHub](https://github.com/rawdash/rawdash)
120
120
 
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", "eo_event"];
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
- unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
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
- cost_in_local_currency: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
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
- unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
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`, `conversions`, and `costEstimated` are carried as attributes.";
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 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.";
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 conversion events on the day.";
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
- unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
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
- cost_in_local_currency: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
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
- unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
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
- cost_in_local_currency: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
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
- unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
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
- cost_in_local_currency: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
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
- unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
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`, `conversions`, and `costEstimated` are carried as attributes.";
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 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.";
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 conversion events on the day.";
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
- unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
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
- cost_in_local_currency: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
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
- unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
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
- cost_in_local_currency: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
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
- unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
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
- cost_in_local_currency: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
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
- unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
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
- unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
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
- cost_in_local_currency: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
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
- unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
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
- cost_in_local_currency: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
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
- unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
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
- cost_in_local_currency: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
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
- unique_count: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodNull]>>;
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 Cross-Platform Analytics API for mobile attribution dashboards.",
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, used together to authenticate Cross-Platform Analytics API requests.",
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 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.",
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 Aggregate API to keep result cardinality bounded.",
67
- "Cost attribution is best-effort - Branch only exposes `cost_in_local_currency` for ad-network-integrated channels. Rows without cost data carry `costEstimated: 0`.",
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 = ["eo_install", "eo_open", "eo_event"];
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
- eo_event: "conversions"
102
+ eo_custom_event: "conversions"
97
103
  };
98
- var isoDateString = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
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
- unique_count: numericLike,
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
- cost_in_local_currency: numericLike
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
- unique_count: numericLike,
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`, `conversions`, and `costEstimated` are carried as attributes.",
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 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.",
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 conversion events on the day."
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.result.timestamp);
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.result.timestamp);
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 res = await this.post(ANALYTICS_API_URL, {
348
- resource,
349
- headers: this.buildHeaders(),
350
- body: this.buildBody(dataSource, dimensions, window),
351
- signal
352
- });
353
- return res.body;
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(window, signal) {
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
- eo_event: []
395
+ eo_custom_event: []
361
396
  };
362
- for (const dataSource of INSTALL_DATA_SOURCES) {
363
- const field = COUNT_FIELD_BY_DATA_SOURCE[dataSource];
364
- const tag = `install_metrics_${field}`;
365
- const body = await this.fetchAggregate(
366
- tag,
367
- dataSource,
368
- dims,
369
- window,
370
- signal
371
- );
372
- rowsByDataSource[dataSource] = body.results ?? [];
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(window, signal) {
377
- const body = await this.fetchAggregate(
378
- "deep_link_events",
379
- "eo_click",
380
- [CHANNEL_DIMENSION, CAMPAIGN_DIMENSION, FEATURE_DIMENSION],
381
- window,
382
- signal
383
- );
384
- return body.results ?? [];
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(window, signal);
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(window, signal);
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.24.0",
3
+ "version": "0.26.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.24.0"
27
+ "@rawdash/core": "0.26.0"
28
28
  },
29
29
  "devDependencies": {
30
30
  "fast-check": "^4.8.0",