@bananalytics/react-native 0.1.0 → 0.2.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/CHANGELOG.md +46 -0
- package/README.md +21 -6
- package/dist/context/session.js +7 -6
- package/dist/core/client.d.ts +23 -0
- package/dist/core/client.js +44 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +16 -0
- package/dist/transport/batcher.js +28 -6
- package/dist/transport/transport.d.ts +8 -0
- package/dist/transport/transport.js +27 -1
- package/dist/utils/network.d.ts +12 -2
- package/dist/utils/network.js +14 -17
- package/dist/utils/validation.js +5 -1
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
A bug-fix release, but the minor version moves because two of the fixes change
|
|
6
|
+
what the SDK sends. Upgrading is strongly recommended: 0.1.x can lose events.
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- **A long offline queue was discarded instead of sent.** `flush()` put the
|
|
11
|
+
whole queue in one request. The queue holds up to 1000 events by default and
|
|
12
|
+
the server accepts 500, rejecting anything larger with a `400` — which is
|
|
13
|
+
treated as non-retryable and dropped. A device offline long enough to queue
|
|
14
|
+
more than 500 events lost all of them on reconnect, silently, and could not
|
|
15
|
+
recover: the persisted queue reloaded on restart and failed the same way.
|
|
16
|
+
Events are now sent in chunks of 500, oldest first and sequentially, so their
|
|
17
|
+
order survives. A failing chunk re-queues from itself onward.
|
|
18
|
+
|
|
19
|
+
- **Events the server rejected were reported as success.** A `200` does not mean
|
|
20
|
+
every event was stored — each one is validated and the rest are named in the
|
|
21
|
+
response body. That body was discarded, so an app could lose part of one event
|
|
22
|
+
type to a malformed name with nothing anywhere saying so. Rejections are now
|
|
23
|
+
logged with the server's reason.
|
|
24
|
+
|
|
25
|
+
- **No event was recorded when the app cold-started.** Lifecycle tracking only
|
|
26
|
+
ever saw `AppState` transitions, which a fresh launch does not produce — so
|
|
27
|
+
the first event of a launch was whatever the person happened to tap, and the
|
|
28
|
+
top of every funnel was short by everyone who opened the app and left.
|
|
29
|
+
|
|
30
|
+
- **The SDK sent a request to Google on every flush.** Without
|
|
31
|
+
`@react-native-community/netinfo` installed, the connectivity check fetched a
|
|
32
|
+
Google endpoint — and returned "online" whether that succeeded or failed, so
|
|
33
|
+
it decided nothing. Removed; a failed send was already handled by the retry
|
|
34
|
+
and the queue.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- `$app_opened` is emitted once per launch when `trackAppLifecycle` is on. If
|
|
39
|
+
you count events per session, expect one more.
|
|
40
|
+
- `optOut()` now discards anything still queued, in memory and on disk. It
|
|
41
|
+
previously only stopped new collection, so already-collected events went out
|
|
42
|
+
on the next flush — which is exactly the data somebody opting out means.
|
|
43
|
+
|
|
44
|
+
## 0.1.1
|
|
45
|
+
|
|
46
|
+
- Initial published release.
|
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ interface BananalyticsConfig {
|
|
|
60
60
|
maxQueueSize?: number; // Max events in memory (default: 1000)
|
|
61
61
|
maxRetries?: number; // Retry attempts (default: 3)
|
|
62
62
|
debug?: boolean; // Enable console logging (default: false)
|
|
63
|
-
trackAppLifecycle?: boolean; // Auto-track foreground/background (default: true)
|
|
63
|
+
trackAppLifecycle?: boolean; // Auto-track app open + foreground/background (default: true)
|
|
64
64
|
trackScreens?: boolean; // Auto-track screen views (default: false)
|
|
65
65
|
sessionTimeout?: number; // Session timeout in ms (default: 1800000)
|
|
66
66
|
}
|
|
@@ -73,19 +73,34 @@ interface BananalyticsConfig {
|
|
|
73
73
|
| `Bananalytics.init(config)` | Initialize the SDK |
|
|
74
74
|
| `Bananalytics.track(event, properties?)` | Track a custom event |
|
|
75
75
|
| `Bananalytics.screen(name, properties?)` | Track a screen view |
|
|
76
|
-
| `Bananalytics.
|
|
76
|
+
| `Bananalytics.trackRevenue(amount, currency?, properties?, event?)` | Track a purchase. Adds `revenue` and `currency` to the properties, which the server reads into its own columns |
|
|
77
|
+
| `Bananalytics.identify(userId, traits?)` | Identify the current user. Events sent before this are attributed to the same person afterwards |
|
|
77
78
|
| `Bananalytics.reset()` | Clear identity and generate new anonymous ID |
|
|
78
79
|
| `Bananalytics.optIn()` | Resume tracking |
|
|
79
|
-
| `Bananalytics.optOut()` | Stop
|
|
80
|
+
| `Bananalytics.optOut()` | Stop tracking and discard anything still queued |
|
|
80
81
|
| `Bananalytics.flush()` | Manually flush queued events |
|
|
81
82
|
|
|
83
|
+
Any event can carry revenue — adding `revenue` and `currency` to the properties
|
|
84
|
+
of your own purchase event does the same thing as `trackRevenue`.
|
|
85
|
+
|
|
86
|
+
## Automatically captured
|
|
87
|
+
|
|
88
|
+
With `trackAppLifecycle` on (the default):
|
|
89
|
+
|
|
90
|
+
| Event | When |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `$app_opened` | The app starts. A cold start is not a foreground transition, so it needs its own event |
|
|
93
|
+
| `$app_foreground` | The app returns from the background |
|
|
94
|
+
| `$app_background` | The app goes to the background. The queue is persisted and flushed here |
|
|
95
|
+
| `$session_start` / `$session_end` | A session begins or times out (`sessionTimeout`, default 30 min) |
|
|
96
|
+
|
|
82
97
|
## Features
|
|
83
98
|
|
|
84
|
-
- Automatic event batching and flushing
|
|
85
|
-
- Offline persistence with AsyncStorage
|
|
99
|
+
- Automatic event batching and flushing, split into requests the server accepts
|
|
100
|
+
- Offline persistence with AsyncStorage, order preserved across restarts
|
|
86
101
|
- Exponential backoff retry on network failures
|
|
87
102
|
- Session tracking with configurable timeout
|
|
88
|
-
- Privacy controls (opt-in/opt-out)
|
|
103
|
+
- Privacy controls (opt-in/opt-out), and no requests to anyone but your server
|
|
89
104
|
- Zero uncaught exceptions (host app stability is sacred)
|
|
90
105
|
|
|
91
106
|
## License
|
package/dist/context/session.js
CHANGED
|
@@ -67,12 +67,13 @@ class SessionManager {
|
|
|
67
67
|
}
|
|
68
68
|
/** Ends the current session. */
|
|
69
69
|
endSession() {
|
|
70
|
-
if (this.session)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
if (!this.session)
|
|
71
|
+
return;
|
|
72
|
+
const ended = this.session;
|
|
73
|
+
this.session = null;
|
|
74
|
+
this.logger.debug('Session ended', ended.id);
|
|
75
|
+
if (this.onSessionEnd) {
|
|
76
|
+
this.onSessionEnd(ended);
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
startNewSession(timestamp) {
|
package/dist/core/client.d.ts
CHANGED
|
@@ -58,6 +58,24 @@ export declare class BananalyticsClient {
|
|
|
58
58
|
* ```
|
|
59
59
|
*/
|
|
60
60
|
screen(screenName: string, properties?: Properties): void;
|
|
61
|
+
/**
|
|
62
|
+
* Tracks a purchase or any other event that earned money.
|
|
63
|
+
*
|
|
64
|
+
* The server reads `revenue` and `currency` out of the properties of *any*
|
|
65
|
+
* event, so an app that already tracks its own purchase event can simply add
|
|
66
|
+
* those two properties instead of switching to this method.
|
|
67
|
+
*
|
|
68
|
+
* @param amount - The monetary value. Negative amounts record refunds.
|
|
69
|
+
* @param currency - ISO 4217 code, e.g. 'USD' or 'EUR'
|
|
70
|
+
* @param properties - Optional extra properties, such as the product ID
|
|
71
|
+
* @param eventName - Event name to record it under
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* client.trackRevenue(9.99, 'EUR', { product_id: 'pro_monthly' });
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
trackRevenue(amount: number, currency?: string, properties?: Properties, eventName?: string): void;
|
|
61
79
|
/**
|
|
62
80
|
* Identifies the current user.
|
|
63
81
|
*
|
|
@@ -85,6 +103,11 @@ export declare class BananalyticsClient {
|
|
|
85
103
|
optIn(): void;
|
|
86
104
|
/**
|
|
87
105
|
* Opts the user out of analytics tracking. Stops all event collection.
|
|
106
|
+
*
|
|
107
|
+
* Anything already queued is discarded rather than sent. Somebody opting out
|
|
108
|
+
* means the data collected up to that moment too — it is still sitting on
|
|
109
|
+
* their device, and a queue that keeps draining afterwards would deliver
|
|
110
|
+
* exactly what they asked not to be delivered.
|
|
88
111
|
*/
|
|
89
112
|
optOut(): void;
|
|
90
113
|
/**
|
package/dist/core/client.js
CHANGED
|
@@ -83,6 +83,17 @@ class BananalyticsClient {
|
|
|
83
83
|
}
|
|
84
84
|
this.batcher.start();
|
|
85
85
|
this.initialized = true;
|
|
86
|
+
// A cold start is not an AppState transition, so the lifecycle tracker
|
|
87
|
+
// never sees it — it only ever reports coming *back* from the background.
|
|
88
|
+
// Without this the first event of a fresh launch is whatever the person
|
|
89
|
+
// happened to tap, and the top of every funnel is short by everyone who
|
|
90
|
+
// opened the app and left again.
|
|
91
|
+
//
|
|
92
|
+
// Fired after identity and session have been restored, so it carries the
|
|
93
|
+
// same anonymous ID as the rest of the launch rather than a fresh one.
|
|
94
|
+
if (this.config.trackAppLifecycle) {
|
|
95
|
+
this.track('$app_opened');
|
|
96
|
+
}
|
|
86
97
|
this.logger.debug('Bananalytics SDK initialized');
|
|
87
98
|
}
|
|
88
99
|
catch (err) {
|
|
@@ -135,6 +146,30 @@ class BananalyticsClient {
|
|
|
135
146
|
this.logger.error('Failed to track screen', err);
|
|
136
147
|
}
|
|
137
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Tracks a purchase or any other event that earned money.
|
|
151
|
+
*
|
|
152
|
+
* The server reads `revenue` and `currency` out of the properties of *any*
|
|
153
|
+
* event, so an app that already tracks its own purchase event can simply add
|
|
154
|
+
* those two properties instead of switching to this method.
|
|
155
|
+
*
|
|
156
|
+
* @param amount - The monetary value. Negative amounts record refunds.
|
|
157
|
+
* @param currency - ISO 4217 code, e.g. 'USD' or 'EUR'
|
|
158
|
+
* @param properties - Optional extra properties, such as the product ID
|
|
159
|
+
* @param eventName - Event name to record it under
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```ts
|
|
163
|
+
* client.trackRevenue(9.99, 'EUR', { product_id: 'pro_monthly' });
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
trackRevenue(amount, currency = 'USD', properties, eventName = '$purchase') {
|
|
167
|
+
if (!Number.isFinite(amount)) {
|
|
168
|
+
this.logger.error('Revenue amount must be a finite number', amount);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
this.track(eventName, { ...properties, revenue: amount, currency });
|
|
172
|
+
}
|
|
138
173
|
/**
|
|
139
174
|
* Identifies the current user.
|
|
140
175
|
*
|
|
@@ -193,11 +228,20 @@ class BananalyticsClient {
|
|
|
193
228
|
}
|
|
194
229
|
/**
|
|
195
230
|
* Opts the user out of analytics tracking. Stops all event collection.
|
|
231
|
+
*
|
|
232
|
+
* Anything already queued is discarded rather than sent. Somebody opting out
|
|
233
|
+
* means the data collected up to that moment too — it is still sitting on
|
|
234
|
+
* their device, and a queue that keeps draining afterwards would deliver
|
|
235
|
+
* exactly what they asked not to be delivered.
|
|
196
236
|
*/
|
|
197
237
|
optOut() {
|
|
198
238
|
this.consent.optOut().catch((err) => {
|
|
199
239
|
this.logger.error('Failed to opt out', err);
|
|
200
240
|
});
|
|
241
|
+
this.queue.clear();
|
|
242
|
+
this.persister.clearQueue().catch((err) => {
|
|
243
|
+
this.logger.error('Failed to clear persisted queue on opt-out', err);
|
|
244
|
+
});
|
|
201
245
|
}
|
|
202
246
|
/**
|
|
203
247
|
* Manually flushes all queued events to the backend.
|
package/dist/index.d.ts
CHANGED
|
@@ -43,6 +43,20 @@ export declare const Bananalytics: {
|
|
|
43
43
|
* @param properties - Optional screen properties
|
|
44
44
|
*/
|
|
45
45
|
screen(screenName: string, properties?: Properties): void;
|
|
46
|
+
/**
|
|
47
|
+
* Tracks a purchase or any other event that earned money.
|
|
48
|
+
*
|
|
49
|
+
* @param amount - The monetary value. Negative amounts record refunds.
|
|
50
|
+
* @param currency - ISO 4217 code, e.g. 'USD' or 'EUR'
|
|
51
|
+
* @param properties - Optional extra properties, such as the product ID
|
|
52
|
+
* @param eventName - Event name to record it under
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* Bananalytics.trackRevenue(9.99, 'EUR', { product_id: 'pro_monthly' });
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
trackRevenue(amount: number, currency?: string, properties?: Properties, eventName?: string): void;
|
|
46
60
|
/**
|
|
47
61
|
* Identifies the current user.
|
|
48
62
|
*
|
package/dist/index.js
CHANGED
|
@@ -69,6 +69,22 @@ exports.Bananalytics = {
|
|
|
69
69
|
screen(screenName, properties) {
|
|
70
70
|
instance?.screen(screenName, properties);
|
|
71
71
|
},
|
|
72
|
+
/**
|
|
73
|
+
* Tracks a purchase or any other event that earned money.
|
|
74
|
+
*
|
|
75
|
+
* @param amount - The monetary value. Negative amounts record refunds.
|
|
76
|
+
* @param currency - ISO 4217 code, e.g. 'USD' or 'EUR'
|
|
77
|
+
* @param properties - Optional extra properties, such as the product ID
|
|
78
|
+
* @param eventName - Event name to record it under
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* Bananalytics.trackRevenue(9.99, 'EUR', { product_id: 'pro_monthly' });
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
trackRevenue(amount, currency, properties, eventName) {
|
|
86
|
+
instance?.trackRevenue(amount, currency, properties, eventName);
|
|
87
|
+
},
|
|
72
88
|
/**
|
|
73
89
|
* Identifies the current user.
|
|
74
90
|
*
|
|
@@ -3,6 +3,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.Batcher = void 0;
|
|
4
4
|
const network_1 = require("../utils/network");
|
|
5
5
|
const retry_1 = require("./retry");
|
|
6
|
+
/**
|
|
7
|
+
* Most events the server accepts in a single request.
|
|
8
|
+
*
|
|
9
|
+
* Mirrors `domain.MaxBatchSize`. Going over it does not get the batch trimmed,
|
|
10
|
+
* it gets the whole request rejected with a 400 — and a 400 is deliberately not
|
|
11
|
+
* retried, so an oversized flush does not fail, it deletes. The default queue
|
|
12
|
+
* holds up to 1000 events, which a device offline for an afternoon reaches
|
|
13
|
+
* easily: precisely the case the queue exists for.
|
|
14
|
+
*/
|
|
15
|
+
const MAX_BATCH_SIZE = 500;
|
|
6
16
|
/**
|
|
7
17
|
* Manages automatic batching and flushing of events.
|
|
8
18
|
* Flushes on a timer interval or when the queue reaches the threshold.
|
|
@@ -76,12 +86,24 @@ class Batcher {
|
|
|
76
86
|
this.flushing = true;
|
|
77
87
|
const events = this.queue.flush();
|
|
78
88
|
try {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
// Oldest first, one request at a time. Sending them in parallel would be
|
|
90
|
+
// faster and would also let a later batch land before an earlier one,
|
|
91
|
+
// which is the ordering the persisted queue exists to preserve.
|
|
92
|
+
for (let sent = 0; sent < events.length; sent += MAX_BATCH_SIZE) {
|
|
93
|
+
const chunk = events.slice(sent, sent + MAX_BATCH_SIZE);
|
|
94
|
+
try {
|
|
95
|
+
await (0, retry_1.withRetry)(() => this.transport.send(chunk), this.maxRetries, this.logger);
|
|
96
|
+
this.logger.debug(`Flushed ${chunk.length} events`);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
// Put back this chunk and everything after it, then stop: whatever
|
|
100
|
+
// stopped this request will stop the next one too, and the events
|
|
101
|
+
// already accepted must not be sent twice.
|
|
102
|
+
this.logger.error('Flush failed after retries, re-queueing events', err);
|
|
103
|
+
this.queue.unshift(events.slice(sent));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
85
107
|
}
|
|
86
108
|
finally {
|
|
87
109
|
this.flushing = false;
|
|
@@ -20,4 +20,12 @@ export declare class Transport {
|
|
|
20
20
|
* ```
|
|
21
21
|
*/
|
|
22
22
|
send(events: EventPayload[]): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Warns about events the server accepted the request for but did not store.
|
|
25
|
+
*
|
|
26
|
+
* Deliberately never throws: the request succeeded, the good events are
|
|
27
|
+
* saved, and re-sending the batch would only duplicate them. An unreadable
|
|
28
|
+
* body is not worth turning a successful flush into a failure either.
|
|
29
|
+
*/
|
|
30
|
+
private reportRejected;
|
|
23
31
|
}
|
|
@@ -38,7 +38,12 @@ class Transport {
|
|
|
38
38
|
if (!response.ok) {
|
|
39
39
|
throw new errors_1.NetworkError(`Ingestion failed with status ${response.status}`, response.status);
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
// A 200 does not mean every event was stored. The server validates each
|
|
42
|
+
// one and keeps the good ones, reporting the rest in the body — usually a
|
|
43
|
+
// malformed event name or a property it could not accept. Dropping that
|
|
44
|
+
// detail on the floor is how an app ends up missing a third of one event
|
|
45
|
+
// type with nothing anywhere saying so.
|
|
46
|
+
await this.reportRejected(response, events.length);
|
|
42
47
|
}
|
|
43
48
|
catch (err) {
|
|
44
49
|
if (err instanceof errors_1.NetworkError) {
|
|
@@ -47,5 +52,26 @@ class Transport {
|
|
|
47
52
|
throw new errors_1.NetworkError(`Network request failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
48
53
|
}
|
|
49
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Warns about events the server accepted the request for but did not store.
|
|
57
|
+
*
|
|
58
|
+
* Deliberately never throws: the request succeeded, the good events are
|
|
59
|
+
* saved, and re-sending the batch would only duplicate them. An unreadable
|
|
60
|
+
* body is not worth turning a successful flush into a failure either.
|
|
61
|
+
*/
|
|
62
|
+
async reportRejected(response, sent) {
|
|
63
|
+
try {
|
|
64
|
+
const body = (await response.json());
|
|
65
|
+
if (body.rejected && body.rejected > 0) {
|
|
66
|
+
this.logger.warn(`Server rejected ${body.rejected} of ${sent} events: ` +
|
|
67
|
+
(body.errors?.join('; ') ?? 'no reason given'));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
this.logger.debug(`Successfully sent ${body.accepted ?? sent} events`);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
this.logger.debug(`Successfully sent ${sent} events`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
50
76
|
}
|
|
51
77
|
exports.Transport = Transport;
|
package/dist/utils/network.d.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Checks if the device currently has network connectivity.
|
|
3
|
-
* Uses React Native's NetInfo if available, falls back to assuming online.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
4
|
+
* Uses React Native's NetInfo when the host app has it. Without it there is no
|
|
5
|
+
* way to ask, so the answer is yes: a failed send is already handled — it
|
|
6
|
+
* retries with backoff and the events stay queued — whereas wrongly deciding
|
|
7
|
+
* the device is offline would stop a flush that would have worked.
|
|
8
|
+
*
|
|
9
|
+
* There used to be a fallback here that fetched a Google endpoint to test the
|
|
10
|
+
* connection. It was removed: it returned true whether it succeeded or failed,
|
|
11
|
+
* so it changed no outcome, and it sent a request to a third party on every
|
|
12
|
+
* flush interval — from a self-hosted, privacy-first SDK whose whole point is
|
|
13
|
+
* that the data goes to your server and nowhere else.
|
|
14
|
+
*
|
|
15
|
+
* @returns true if connected, false if NetInfo says otherwise
|
|
6
16
|
*/
|
|
7
17
|
export declare function isOnline(): Promise<boolean>;
|
package/dist/utils/network.js
CHANGED
|
@@ -3,31 +3,28 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.isOnline = isOnline;
|
|
4
4
|
/**
|
|
5
5
|
* Checks if the device currently has network connectivity.
|
|
6
|
-
* Uses React Native's NetInfo if available, falls back to assuming online.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
7
|
+
* Uses React Native's NetInfo when the host app has it. Without it there is no
|
|
8
|
+
* way to ask, so the answer is yes: a failed send is already handled — it
|
|
9
|
+
* retries with backoff and the events stay queued — whereas wrongly deciding
|
|
10
|
+
* the device is offline would stop a flush that would have worked.
|
|
11
|
+
*
|
|
12
|
+
* There used to be a fallback here that fetched a Google endpoint to test the
|
|
13
|
+
* connection. It was removed: it returned true whether it succeeded or failed,
|
|
14
|
+
* so it changed no outcome, and it sent a request to a third party on every
|
|
15
|
+
* flush interval — from a self-hosted, privacy-first SDK whose whole point is
|
|
16
|
+
* that the data goes to your server and nowhere else.
|
|
17
|
+
*
|
|
18
|
+
* @returns true if connected, false if NetInfo says otherwise
|
|
9
19
|
*/
|
|
10
20
|
async function isOnline() {
|
|
11
21
|
try {
|
|
12
|
-
//
|
|
22
|
+
// Requires @react-native-community/netinfo in the host app.
|
|
13
23
|
const NetInfo = require('@react-native-community/netinfo');
|
|
14
24
|
const state = await NetInfo.fetch();
|
|
15
25
|
return state.isConnected ?? true;
|
|
16
26
|
}
|
|
17
27
|
catch {
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
const controller = new AbortController();
|
|
21
|
-
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
22
|
-
await fetch('https://clients3.google.com/generate_204', {
|
|
23
|
-
method: 'HEAD',
|
|
24
|
-
signal: controller.signal,
|
|
25
|
-
});
|
|
26
|
-
clearTimeout(timeout);
|
|
27
|
-
return true;
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
return true; // Assume online if we can't determine — better to try and fail
|
|
31
|
-
}
|
|
28
|
+
return true;
|
|
32
29
|
}
|
|
33
30
|
}
|
package/dist/utils/validation.js
CHANGED
|
@@ -51,7 +51,11 @@ function validateProperties(properties) {
|
|
|
51
51
|
if (key.length > MAX_PROPERTY_KEY_LENGTH) {
|
|
52
52
|
return `property key "${key}" exceeds ${MAX_PROPERTY_KEY_LENGTH} character limit`;
|
|
53
53
|
}
|
|
54
|
-
const
|
|
54
|
+
const value = properties[key];
|
|
55
|
+
if (value === undefined || value === null) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const serialized = JSON.stringify(value);
|
|
55
59
|
if (serialized.length > MAX_PROPERTY_VALUE_SIZE) {
|
|
56
60
|
return `property value for key "${key}" exceeds ${MAX_PROPERTY_VALUE_SIZE} byte limit: got ${serialized.length}`;
|
|
57
61
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananalytics/react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Self-hosted, privacy-first analytics SDK for React Native and Expo apps. Funnels, retention, sessions, geography — your server, your data.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"files": [
|
|
8
8
|
"dist",
|
|
9
|
+
"CHANGELOG.md",
|
|
9
10
|
"README.md",
|
|
10
11
|
"LICENSE"
|
|
11
12
|
],
|