react-native-test-adx 0.1.13 → 0.1.14

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
@@ -75,9 +75,15 @@ Pass `configuration` to `AdaxonProvider`, or to `Adaxon.initialize()` when not u
75
75
  | `storage` | No | In-memory | Persists offline event queue and anonymous id |
76
76
  | `timeoutMs` | No | `10000` | HTTP timeout in ms |
77
77
  | `testMode` | No | `false` | Sets `sdk_version` to `{version}-test` when true |
78
+ | `activityLogging` | No | `false` | When true, records SDK diagnostics to Sentry (testing/debugging) |
79
+ | `sentryDsn` | No | — | Sentry DSN when `activityLogging` is true and the host app has not initialized Sentry |
78
80
 
79
81
  Only `appId` and `sdkKey` are required. Do not pass a tracker URL — the SDK owns the host and API paths.
80
82
 
83
+ For Sentry activity logging, install the optional peer dependency `@sentry/react-native` in the host app.
84
+
85
+ Activities are emitted primarily as **Sentry structured logs** (`enableLogs: true` when the SDK initializes Sentry). Breadcrumbs are added only for `warn`/`error` to enrich captured issues. If structured logging is unavailable (for example an older host Sentry client), a small set of milestone activities may use `captureMessage` instead so they still appear independently without flooding issue volume.
86
+
81
87
  The SDK attaches `device`, `app`, `user.anonymous_id`, and network fields automatically. Do not pass them in event calls.
82
88
 
83
89
  ### Persistent storage (optional)
@@ -7,6 +7,7 @@ import { AdaxonSdkError } from "./errors/AdaxonSdkError.js";
7
7
  import { detectReactNativePlatform, reactNativeAppInfoReader, reactNativeDeviceReader, reactNativeNetworkInfoReader, reactNativeNetworkStatusReader, reactNativeUrlOpener } from "./platform/reactNative.js";
8
8
  import { buildAddToCartProperties, buildPurchaseProperties, normalizeEventValues } from "./services/event/normalizeProperties.js";
9
9
  import { AdxEvent } from "./services/event/predefined.js";
10
+ import { AdaxonLogger, logSdkActivity } from "./services/logging/AdaxonLogger.js";
10
11
  function openableOfferUrl(url) {
11
12
  return url.replace(/[{}]/g, brace => brace === '{' ? '%7B' : '%7D');
12
13
  }
@@ -18,11 +19,26 @@ export class AdaxonSdk {
18
19
  return this.client !== null;
19
20
  }
20
21
  async initialize(configuration, listener) {
22
+ const platform = detectReactNativePlatform();
23
+ AdaxonLogger.configure({
24
+ enabled: Boolean(configuration.activityLogging),
25
+ dsn: configuration.sentryDsn,
26
+ platform,
27
+ testMode: Boolean(configuration.testMode)
28
+ });
29
+ logSdkActivity({
30
+ eventName: 'sdk.initialize.started',
31
+ level: 'info',
32
+ metadata: {
33
+ app_id: configuration.appId,
34
+ test_mode: Boolean(configuration.testMode)
35
+ }
36
+ });
21
37
  try {
22
38
  const previous = this.client;
23
39
  const client = new AdaxonClient({
24
40
  ...configuration,
25
- platform: detectReactNativePlatform(),
41
+ platform,
26
42
  deviceInfoReader: reactNativeDeviceReader,
27
43
  appInfoReader: reactNativeAppInfoReader,
28
44
  networkInfoReader: reactNativeNetworkInfoReader,
@@ -32,15 +48,31 @@ export class AdaxonSdk {
32
48
  previous?.destroy();
33
49
  this.client = client;
34
50
  this.testMode = Boolean(configuration.testMode);
51
+ logSdkActivity({
52
+ eventName: 'sdk.initialize.completed',
53
+ level: 'info',
54
+ metadata: {
55
+ network_available: client.isNetworkAvailable()
56
+ }
57
+ });
35
58
  listener?.onInitializationComplete();
36
59
  if (client.isNetworkAvailable()) {
37
60
  client.processPendingEvents().catch(() => undefined);
38
61
  }
39
62
  } catch (error) {
40
- this.destroy();
41
63
  const message = AdaxonSdkError.messageOf(error, 'Initialization failed.');
42
64
  const code = error instanceof AdaxonSdkError && error.code === 'INVALID_CONFIG' ? 'INVALID_CONFIG' : 'UNKNOWN';
65
+ logSdkActivity({
66
+ eventName: 'sdk.initialize.failed',
67
+ level: code === 'INVALID_CONFIG' ? 'warn' : 'error',
68
+ metadata: {
69
+ error_code: code
70
+ },
71
+ error,
72
+ captureAsEvent: code !== 'INVALID_CONFIG'
73
+ });
43
74
  listener?.onInitializationFailed(code, message);
75
+ this.destroy();
44
76
  throw error;
45
77
  }
46
78
  }
@@ -75,6 +107,10 @@ export class AdaxonSdk {
75
107
  return this.logEvent(AdxEvent.COMPLETE_REGISTRATION, params, options);
76
108
  }
77
109
  async showAd(options = {}) {
110
+ logSdkActivity({
111
+ eventName: 'ad.show.started',
112
+ level: 'info'
113
+ });
78
114
  try {
79
115
  const {
80
116
  offers,
@@ -82,6 +118,13 @@ export class AdaxonSdk {
82
118
  } = await this.loadRecommendations();
83
119
  if (offers.length === 0) {
84
120
  console.warn('[Adaxon] No offer available.');
121
+ logSdkActivity({
122
+ eventName: 'ad.show.failed',
123
+ level: 'info',
124
+ metadata: {
125
+ reason: 'no_offer'
126
+ }
127
+ });
85
128
  return {
86
129
  shown: false,
87
130
  reason: 'no_offer'
@@ -91,6 +134,13 @@ export class AdaxonSdk {
91
134
  const offer = offers.find(item => !excluded.has(item.offer_id));
92
135
  if (!offer) {
93
136
  console.warn('[Adaxon] No eligible offer available.');
137
+ logSdkActivity({
138
+ eventName: 'ad.show.failed',
139
+ level: 'info',
140
+ metadata: {
141
+ reason: 'no_eligible_offer'
142
+ }
143
+ });
94
144
  return {
95
145
  shown: false,
96
146
  reason: 'no_eligible_offer'
@@ -100,11 +150,34 @@ export class AdaxonSdk {
100
150
  const displayed = presentInterstitial({
101
151
  offer,
102
152
  interact: () => {
153
+ logSdkActivity({
154
+ eventName: 'ad.click',
155
+ level: 'info',
156
+ metadata: {
157
+ offer_id: offer.offer_id
158
+ }
159
+ });
103
160
  this.openOfferUrl(offer, requestEventId).then(() => options.onOfferClicked?.(offer.offer_id)).catch(error => {
104
161
  console.warn('[Adaxon] Failed to open offer URL.', AdaxonSdkError.messageOf(error, 'Failed to open offer.'));
162
+ logSdkActivity({
163
+ eventName: 'ad.click.failed',
164
+ level: 'warn',
165
+ metadata: {
166
+ offer_id: offer.offer_id
167
+ },
168
+ error
169
+ });
105
170
  });
106
171
  },
107
172
  onDismiss: dismissReason => {
173
+ logSdkActivity({
174
+ eventName: 'ad.dismissed',
175
+ level: 'info',
176
+ metadata: {
177
+ offer_id: offer.offer_id,
178
+ dismiss_reason: dismissReason
179
+ }
180
+ });
108
181
  resolve({
109
182
  shown: true,
110
183
  offerId: offer.offer_id,
@@ -114,16 +187,38 @@ export class AdaxonSdk {
114
187
  });
115
188
  if (!displayed) {
116
189
  console.warn('[Adaxon] Interstitial host is not mounted. Wrap the app in AdaxonProvider.');
190
+ logSdkActivity({
191
+ eventName: 'ad.show.failed',
192
+ level: 'warn',
193
+ metadata: {
194
+ reason: 'not_mounted'
195
+ }
196
+ });
117
197
  resolve({
118
198
  shown: false,
119
199
  reason: 'not_mounted'
120
200
  });
121
201
  return;
122
202
  }
203
+ logSdkActivity({
204
+ eventName: 'ad.displayed',
205
+ level: 'info',
206
+ metadata: {
207
+ offer_id: offer.offer_id
208
+ }
209
+ });
123
210
  this.trackRecommendationEvent(CREATIVE_VIEW_EVENT, requestEventId).catch(() => undefined);
124
211
  });
125
212
  } catch (error) {
126
213
  console.warn('[Adaxon] Failed to show ad.', AdaxonSdkError.messageOf(error, 'Failed to show ad.'));
214
+ logSdkActivity({
215
+ eventName: 'ad.show.failed',
216
+ level: 'warn',
217
+ metadata: {
218
+ error_code: error instanceof AdaxonSdkError ? error.code : 'UNKNOWN'
219
+ },
220
+ error
221
+ });
127
222
  if (this.testMode) {
128
223
  throw error;
129
224
  }
@@ -137,11 +232,16 @@ export class AdaxonSdk {
137
232
  return this.client?.isNetworkAvailable() ?? false;
138
233
  }
139
234
  destroy() {
235
+ logSdkActivity({
236
+ eventName: 'sdk.destroyed',
237
+ level: 'debug'
238
+ });
140
239
  dismissInterstitial();
141
240
  dismissOfferBrowser();
142
241
  this.client?.destroy();
143
242
  this.client = null;
144
243
  this.testMode = false;
244
+ AdaxonLogger.reset();
145
245
  }
146
246
  loadRecommendations() {
147
247
  return this.requireClient().getRecommendations({
@@ -153,8 +253,22 @@ export class AdaxonSdk {
153
253
  await this.trackRecommendationEvent(CREATIVE_CLICK_EVENT, parentEventId);
154
254
  const offerUrl = openableOfferUrl(offer.offer_url);
155
255
  if (presentOfferBrowser(offerUrl)) {
256
+ logSdkActivity({
257
+ eventName: 'ad.offer_browser.opened',
258
+ level: 'debug',
259
+ metadata: {
260
+ offer_id: offer.offer_id
261
+ }
262
+ });
156
263
  return;
157
264
  }
265
+ logSdkActivity({
266
+ eventName: 'ad.offer_url.opened',
267
+ level: 'debug',
268
+ metadata: {
269
+ offer_id: offer.offer_id
270
+ }
271
+ });
158
272
  await reactNativeUrlOpener.openURL(offerUrl);
159
273
  dismissInterstitial('click');
160
274
  }
@@ -165,8 +279,23 @@ export class AdaxonSdk {
165
279
  }, {
166
280
  eventType: RECOMMENDATION_EVENT_TYPE
167
281
  });
282
+ logSdkActivity({
283
+ eventName: eventName === CREATIVE_VIEW_EVENT ? 'ad.impression.tracked' : 'ad.click.tracked',
284
+ level: 'debug',
285
+ metadata: {
286
+ parent_event_id: parentEventId
287
+ }
288
+ });
168
289
  } catch (error) {
169
290
  console.warn(`[Adaxon] Failed to log ${eventName}.`, AdaxonSdkError.messageOf(error, `Failed to log ${eventName}.`));
291
+ logSdkActivity({
292
+ eventName: 'tracking.recommendation_event.failed',
293
+ level: 'warn',
294
+ metadata: {
295
+ mts_event_name: eventName
296
+ },
297
+ error
298
+ });
170
299
  }
171
300
  }
172
301
  requireClient() {
@@ -12,6 +12,7 @@ import { ApiClient } from "../services/network/ApiClient.js";
12
12
  import { NetworkInfoService } from "../services/network/NetworkInfoService.js";
13
13
  import { NetworkService } from "../services/network/NetworkService.js";
14
14
  import { EventQueueStorage } from "../storage/EventQueueStorage.js";
15
+ import { logSdkActivity } from "../services/logging/AdaxonLogger.js";
15
16
  import { EventValidator } from "../validation/EventValidator.js";
16
17
  export class AdaxonClient {
17
18
  started = false;
@@ -73,6 +74,13 @@ export class AdaxonClient {
73
74
  }
74
75
  this.validator.validateBatch(events);
75
76
  const payloads = await Promise.all(events.map(event => this.builder.build(event)));
77
+ logSdkActivity({
78
+ eventName: 'tracking.events.queued_offline',
79
+ level: 'info',
80
+ metadata: {
81
+ event_count: payloads.length
82
+ }
83
+ });
76
84
  for (const payload of payloads) {
77
85
  await this.eventQueue.add(payload);
78
86
  }
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
 
3
3
  /** Keep in sync with package.json "version" (published npm packages ship lib/ only). */
4
- export const SDK_VERSION = '0.1.13';
4
+ export const SDK_VERSION = '0.1.14';
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { DEFAULT_TIMEOUT_MS } from "../../config/ApiConstants.js";
4
4
  import { AdaxonSdkError } from "../../errors/AdaxonSdkError.js";
5
+ import { logSdkActivity } from "../logging/AdaxonLogger.js";
5
6
  export class AuthenticationService {
6
7
  constructor(config) {
7
8
  this.validateConfig(config);
@@ -21,6 +22,13 @@ export class AuthenticationService {
21
22
  }
22
23
  }
23
24
  fail(message) {
25
+ logSdkActivity({
26
+ eventName: 'sdk.config.invalid',
27
+ level: 'warn',
28
+ metadata: {
29
+ reason: message
30
+ }
31
+ });
24
32
  throw new AdaxonSdkError('INVALID_CONFIG', message);
25
33
  }
26
34
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { MAX_BATCH_SIZE } from "../../config/ApiConstants.js";
4
4
  import { AdaxonSdkError } from "../../errors/AdaxonSdkError.js";
5
+ import { logSdkActivity } from "../logging/AdaxonLogger.js";
5
6
  const TEMPORARY_FAILURE_CODES = new Set(['NETWORK_ERROR', 'TIMEOUT', 'HTTP_ERROR', 'AUTHENTICATION_ERROR']);
6
7
  export class BatchProcessor {
7
8
  constructor(config) {
@@ -10,14 +11,30 @@ export class BatchProcessor {
10
11
  }
11
12
  async process() {
12
13
  if (!this.queue.tryAcquireProcessorLock()) {
14
+ logSdkActivity({
15
+ eventName: 'tracking.queue.processor.busy',
16
+ level: 'debug'
17
+ });
13
18
  return this.result('already_processing');
14
19
  }
15
20
  let batchesSent = 0;
16
21
  let eventsProcessed = 0;
22
+ logSdkActivity({
23
+ eventName: 'tracking.queue.processor.started',
24
+ level: 'debug'
25
+ });
17
26
  try {
18
27
  while (true) {
19
28
  const events = await this.queue.getNextBatch(MAX_BATCH_SIZE);
20
29
  if (events.length === 0) {
30
+ logSdkActivity({
31
+ eventName: 'tracking.queue.processor.completed',
32
+ level: 'debug',
33
+ metadata: {
34
+ batches_sent: batchesSent,
35
+ events_processed: eventsProcessed
36
+ }
37
+ });
21
38
  return this.result('completed', {
22
39
  batchesSent,
23
40
  eventsProcessed
@@ -42,8 +59,26 @@ export class BatchProcessor {
42
59
  }
43
60
  }
44
61
  stopped(batchesSent, eventsProcessed, events, error) {
45
- if (error !== undefined && !(error instanceof AdaxonSdkError && TEMPORARY_FAILURE_CODES.has(error.code))) {
46
- console.error('[Adaxon] Batch processor failed unexpectedly', error);
62
+ if (error !== undefined) {
63
+ const temporaryFailure = error instanceof AdaxonSdkError && TEMPORARY_FAILURE_CODES.has(error.code);
64
+ if (!temporaryFailure) {
65
+ console.error('[Adaxon] Batch processor failed unexpectedly', error);
66
+ }
67
+ logSdkActivity({
68
+ eventName: temporaryFailure ? 'tracking.queue.processor.stopped' : 'tracking.queue.processor.failed',
69
+ level: temporaryFailure ? 'warn' : 'error',
70
+ metadata: {
71
+ batches_sent: batchesSent,
72
+ events_processed: eventsProcessed,
73
+ ...(temporaryFailure ? {
74
+ error_code: error instanceof AdaxonSdkError ? error.code : 'UNKNOWN'
75
+ } : {})
76
+ },
77
+ error,
78
+ ...(!temporaryFailure ? {
79
+ captureAsEvent: true
80
+ } : {})
81
+ });
47
82
  }
48
83
  return this.result('stopped', {
49
84
  batchesSent,
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { EVENT_TRACKING_PATH, RECOMMENDATION_EVENT_TYPE, RECOMMENDATION_REQUEST_EVENT, RECOMMENDATION_RESPONSE_EVENT, RECOMMENDATIONS_PATH } from "../../config/ApiConstants.js";
4
4
  import { AdaxonSdkError } from "../../errors/AdaxonSdkError.js";
5
+ import { logSdkActivity } from "../logging/AdaxonLogger.js";
5
6
  export class EventService {
6
7
  constructor(config) {
7
8
  this.api = config.api;
@@ -10,14 +11,35 @@ export class EventService {
10
11
  }
11
12
  async trackBatch(events) {
12
13
  this.validator.validateBatch(events);
14
+ logSdkActivity({
15
+ eventName: 'tracking.batch.processing',
16
+ level: 'debug',
17
+ metadata: {
18
+ event_count: events.length
19
+ }
20
+ });
13
21
  const payloads = await Promise.all(events.map(event => this.builder.build(event)));
14
- return this.postEvents(payloads);
22
+ const response = await this.postEvents(payloads);
23
+ logSdkActivity({
24
+ eventName: 'tracking.batch.completed',
25
+ level: 'debug',
26
+ metadata: {
27
+ event_count: events.length,
28
+ accepted: response.accepted,
29
+ rejected: response.rejected
30
+ }
31
+ });
32
+ return response;
15
33
  }
16
34
  sendPayloads(payloads) {
17
35
  this.validator.validateBatch(payloads);
18
36
  return this.postEvents(payloads);
19
37
  }
20
38
  async getRecommendations(properties = {}) {
39
+ logSdkActivity({
40
+ eventName: 'recommendation.request.started',
41
+ level: 'info'
42
+ });
21
43
  const requestEvent = await this.builder.build({
22
44
  eventName: RECOMMENDATION_REQUEST_EVENT,
23
45
  eventType: RECOMMENDATION_EVENT_TYPE,
@@ -31,8 +53,24 @@ export class EventService {
31
53
  }
32
54
  });
33
55
  if (!Array.isArray(response.offers)) {
34
- throw new AdaxonSdkError('HTTP_ERROR', 'Adaxon recommendation response is missing the offers array.');
56
+ const error = new AdaxonSdkError('HTTP_ERROR', 'Adaxon recommendation response is missing the offers array.');
57
+ logSdkActivity({
58
+ eventName: 'recommendation.response.invalid',
59
+ level: 'warn',
60
+ metadata: {
61
+ error_code: error.code
62
+ },
63
+ error
64
+ });
65
+ throw error;
35
66
  }
67
+ logSdkActivity({
68
+ eventName: 'recommendation.response.received',
69
+ level: 'info',
70
+ metadata: {
71
+ offer_count: response.offers.length
72
+ }
73
+ });
36
74
  await this.postEvents([await this.builder.build({
37
75
  eventName: RECOMMENDATION_RESPONSE_EVENT,
38
76
  eventType: RECOMMENDATION_EVENT_TYPE,
@@ -40,6 +78,13 @@ export class EventService {
40
78
  parent_event_id: requestEvent.event_id
41
79
  }
42
80
  })]);
81
+ logSdkActivity({
82
+ eventName: 'recommendation.processing.completed',
83
+ level: 'debug',
84
+ metadata: {
85
+ offer_count: response.offers.length
86
+ }
87
+ });
43
88
  return {
44
89
  offers: response.offers,
45
90
  requestEventId: requestEvent.event_id
@@ -0,0 +1,227 @@
1
+ "use strict";
2
+
3
+ import { SDK_VERSION } from "../../sdkVersion.js";
4
+ import { AdaxonSdkError } from "../../errors/AdaxonSdkError.js";
5
+ import { sanitizeLogMetadata } from "./sanitizeLogMetadata.js";
6
+ const DEFAULT_LEVEL = 'info';
7
+
8
+ /** Milestone events that fall back to captureMessage when structured logs are unavailable. */
9
+ const MESSAGE_FALLBACK_EVENTS = new Set(['sdk.initialize.completed', 'sdk.initialize.failed', 'recommendation.response.received', 'ad.displayed', 'ad.show.failed']);
10
+ /** undefined = not loaded yet; null = unavailable in this app bundle */
11
+ let cachedSentryModule;
12
+ let missingSentryPeerWarningShown = false;
13
+ const SENTRY_UNAVAILABLE_HINT = 'Bundled @sentry/react-native could not be loaded. Reinstall react-native-adaxon dependencies and rebuild the native app.';
14
+ function loadSentryModule() {
15
+ if (cachedSentryModule !== undefined) {
16
+ return cachedSentryModule;
17
+ }
18
+ try {
19
+ const loaded = require('@sentry/react-native');
20
+ cachedSentryModule = loaded && typeof loaded.init === 'function' ? loaded : loaded.default ?? null;
21
+ } catch {
22
+ cachedSentryModule = null;
23
+ }
24
+ return cachedSentryModule;
25
+ }
26
+ function warnSentryUnavailableOnce(hasDsn) {
27
+ if (!hasDsn || missingSentryPeerWarningShown || typeof __DEV__ === 'undefined' || !__DEV__) {
28
+ return;
29
+ }
30
+ if (loadSentryModule()) {
31
+ return;
32
+ }
33
+ missingSentryPeerWarningShown = true;
34
+ console.warn(`[Adaxon] Activity logging is on and Sentry DSN is set, but ${SENTRY_UNAVAILABLE_HINT} ` + 'Until then, events appear locally as [Adaxon activity] in Metro/logcat only.');
35
+ }
36
+ function sentryLevel(level, target) {
37
+ if (level === 'warn') {
38
+ return 'warning';
39
+ }
40
+ if (level === 'error') {
41
+ return 'error';
42
+ }
43
+ if (level === 'debug') {
44
+ return target === 'breadcrumb' ? 'debug' : 'info';
45
+ }
46
+ return 'info';
47
+ }
48
+ function shouldCaptureMessage(input, level, structuredLogsUsed) {
49
+ if (structuredLogsUsed || level === 'debug') {
50
+ return false;
51
+ }
52
+ if (input.captureAsEvent && input.error === undefined) {
53
+ return true;
54
+ }
55
+ return MESSAGE_FALLBACK_EVENTS.has(input.eventName);
56
+ }
57
+ function usesStructuredSentryLogs(platform) {
58
+ return platform === 'web';
59
+ }
60
+ function emitStructuredLog(sentry, level, eventName, metadata) {
61
+ const loggerFn = sentry.logger?.[level];
62
+ if (typeof loggerFn !== 'function') {
63
+ return false;
64
+ }
65
+ loggerFn.call(sentry.logger, eventName, {
66
+ ...metadata,
67
+ event_name: eventName
68
+ });
69
+ return true;
70
+ }
71
+ function emitNativeActivityBreadcrumb(sentry, level, eventName, metadata) {
72
+ sentry.addBreadcrumb({
73
+ category: 'adaxon.activity',
74
+ message: eventName,
75
+ level: sentryLevel(level, 'breadcrumb'),
76
+ data: metadata,
77
+ timestamp: Date.now() / 1000
78
+ });
79
+ }
80
+ function emitDevActivityFallback(level, eventName, metadata, deliveredToSentry) {
81
+ if (deliveredToSentry || typeof __DEV__ === 'undefined' || !__DEV__) {
82
+ return;
83
+ }
84
+ const consoleFn = level === 'error' ? console.error : level === 'warn' ? console.warn : console.info;
85
+ const write = () => {
86
+ consoleFn(`[Adaxon activity] ${eventName}`, metadata);
87
+ };
88
+ const sentry = loadSentryModule();
89
+ if (typeof sentry?.consoleSandbox === 'function') {
90
+ sentry.consoleSandbox(write);
91
+ return;
92
+ }
93
+ write();
94
+ }
95
+ function activityTags(eventName, platform) {
96
+ return {
97
+ adaxon_event: eventName,
98
+ adaxon_platform: platform,
99
+ adaxon_sdk_version: SDK_VERSION
100
+ };
101
+ }
102
+ class AdaxonLoggerService {
103
+ enabled = false;
104
+ platform = 'web';
105
+ sentryReady = false;
106
+ configure(input) {
107
+ this.enabled = Boolean(input.enabled);
108
+ this.platform = input.platform;
109
+ this.sentryReady = false;
110
+ if (!this.enabled) {
111
+ return;
112
+ }
113
+ warnSentryUnavailableOnce(Boolean(input.dsn?.trim()));
114
+ this.ensureSentry(input.dsn, Boolean(input.testMode));
115
+ }
116
+ reset() {
117
+ this.enabled = false;
118
+ this.sentryReady = false;
119
+ this.platform = 'web';
120
+ cachedSentryModule = undefined;
121
+ missingSentryPeerWarningShown = false;
122
+ }
123
+ logActivity(input) {
124
+ if (!this.enabled) {
125
+ return;
126
+ }
127
+ const level = input.level ?? DEFAULT_LEVEL;
128
+ const metadata = sanitizeLogMetadata({
129
+ ...input.metadata,
130
+ sdk_version: SDK_VERSION,
131
+ platform: this.platform,
132
+ timestamp: new Date().toISOString()
133
+ });
134
+ const tags = activityTags(input.eventName, this.platform);
135
+ let deliveredToSentry = false;
136
+ this.safe(() => {
137
+ const sentry = loadSentryModule();
138
+ if (!sentry || !this.sentryReady) {
139
+ return;
140
+ }
141
+ const structuredLogsUsed = usesStructuredSentryLogs(this.platform);
142
+ let structuredLogSent = false;
143
+ if (structuredLogsUsed) {
144
+ structuredLogSent = emitStructuredLog(sentry, level, input.eventName, metadata);
145
+ deliveredToSentry = structuredLogSent;
146
+ if (level === 'warn' || level === 'error') {
147
+ sentry.addBreadcrumb({
148
+ category: 'adaxon.sdk',
149
+ message: input.eventName,
150
+ level: sentryLevel(level, 'breadcrumb'),
151
+ data: metadata,
152
+ timestamp: Date.now() / 1000
153
+ });
154
+ }
155
+ } else {
156
+ emitNativeActivityBreadcrumb(sentry, level, input.eventName, metadata);
157
+ deliveredToSentry = true;
158
+ }
159
+ if (usesStructuredSentryLogs(this.platform) && shouldCaptureMessage(input, level, structuredLogSent) && typeof sentry.captureMessage === 'function') {
160
+ sentry.captureMessage(input.eventName, {
161
+ level: sentryLevel(level, 'message'),
162
+ tags,
163
+ extra: metadata
164
+ });
165
+ deliveredToSentry = true;
166
+ }
167
+ if (input.captureAsEvent && input.error !== undefined) {
168
+ const captureError = input.error instanceof Error ? input.error : new Error(AdaxonSdkError.messageOf(input.error, `Adaxon SDK activity: ${input.eventName}`));
169
+ sentry.captureException(captureError, {
170
+ tags,
171
+ extra: metadata
172
+ });
173
+ deliveredToSentry = true;
174
+ }
175
+ });
176
+ emitDevActivityFallback(level, input.eventName, metadata, deliveredToSentry);
177
+ }
178
+ ensureSentry(dsn, testMode) {
179
+ this.safe(() => {
180
+ const sentry = loadSentryModule();
181
+ if (!sentry) {
182
+ return;
183
+ }
184
+ if (!sentry.getClient()) {
185
+ if (!dsn?.trim()) {
186
+ return;
187
+ }
188
+ const structuredLogs = usesStructuredSentryLogs(this.platform);
189
+ sentry.init(structuredLogs ? {
190
+ dsn: dsn.trim(),
191
+ environment: testMode ? 'adaxon-sdk-test' : 'adaxon-sdk',
192
+ enableLogs: true,
193
+ enableAutoConsoleLogs: false,
194
+ tracesSampleRate: 0,
195
+ enableAutoSessionTracking: false,
196
+ sendDefaultPii: false
197
+ } : {
198
+ dsn: dsn.trim(),
199
+ environment: testMode ? 'adaxon-sdk-test' : 'adaxon-sdk',
200
+ enableLogs: false,
201
+ enableAutoConsoleLogs: false,
202
+ enableNative: false,
203
+ autoInitializeNativeSdk: false,
204
+ defaultIntegrations: false,
205
+ integrations: [],
206
+ tracesSampleRate: 0,
207
+ enableAutoSessionTracking: false,
208
+ sendDefaultPii: false
209
+ });
210
+ }
211
+ sentry.setTag('adaxon_sdk_version', SDK_VERSION);
212
+ sentry.setTag('adaxon_platform', this.platform);
213
+ this.sentryReady = true;
214
+ });
215
+ }
216
+ safe(run) {
217
+ try {
218
+ run();
219
+ } catch {
220
+ // Logging must never interrupt SDK behavior.
221
+ }
222
+ }
223
+ }
224
+ export const AdaxonLogger = new AdaxonLoggerService();
225
+ export function logSdkActivity(input) {
226
+ AdaxonLogger.logActivity(input);
227
+ }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+
3
+ const SENSITIVE_KEY = /^(sdk[_-]?key|api[_-]?key|authorization|x-sdk-key|password|secret|token|cookie|credentials?|dsn|sentry[_-]?dsn|headers)$/i;
4
+ const MAX_STRING_LENGTH = 500;
5
+ const MAX_NESTED_KEYS = 40;
6
+ function truncate(value) {
7
+ return value.length <= MAX_STRING_LENGTH ? value : `${value.slice(0, MAX_STRING_LENGTH)}…`;
8
+ }
9
+ function sanitizeField(key, value) {
10
+ if (SENSITIVE_KEY.test(key)) {
11
+ return '[redacted]';
12
+ }
13
+ if (value === null || value === undefined) {
14
+ return value;
15
+ }
16
+ if (typeof value === 'string') {
17
+ return truncate(value);
18
+ }
19
+ if (typeof value === 'number' || typeof value === 'boolean') {
20
+ return value;
21
+ }
22
+ if (Array.isArray(value)) {
23
+ return value.map(item => typeof item === 'string' ? truncate(item) : item);
24
+ }
25
+ if (typeof value === 'object') {
26
+ const nested = {};
27
+ for (const [nestedKey, nestedValue] of Object.entries(value).slice(0, MAX_NESTED_KEYS)) {
28
+ nested[nestedKey] = SENSITIVE_KEY.test(nestedKey) ? '[redacted]' : typeof nestedValue === 'string' ? truncate(nestedValue) : nestedValue;
29
+ }
30
+ return nested;
31
+ }
32
+ return String(value);
33
+ }
34
+
35
+ /** Redact secrets and cap string size before activity logs leave the SDK. */
36
+ export function sanitizeLogMetadata(metadata) {
37
+ if (!metadata) {
38
+ return {};
39
+ }
40
+ const output = {};
41
+ for (const [key, value] of Object.entries(metadata)) {
42
+ output[key] = sanitizeField(key, value);
43
+ }
44
+ return output;
45
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { BASE_URL } from "../../config/ApiConstants.js";
4
4
  import { AdaxonSdkError } from "../../errors/AdaxonSdkError.js";
5
+ import { logSdkActivity } from "../logging/AdaxonLogger.js";
5
6
  export class ApiClient {
6
7
  constructor(config) {
7
8
  this.config = config;
@@ -14,6 +15,15 @@ export class ApiClient {
14
15
  async post(path, body) {
15
16
  const controller = new AbortController();
16
17
  const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs);
18
+ const batchSize = Array.isArray(body) ? body.length : undefined;
19
+ logSdkActivity({
20
+ eventName: 'api.request.started',
21
+ level: 'debug',
22
+ metadata: {
23
+ path,
24
+ batch_size: batchSize
25
+ }
26
+ });
17
27
  try {
18
28
  const response = await this.fetchImplementation(`${BASE_URL}${path}`, {
19
29
  method: 'POST',
@@ -27,31 +37,70 @@ export class ApiClient {
27
37
  });
28
38
  const parsedBody = this.parseJson(await response.text());
29
39
  if (!response.ok) {
30
- throw AdaxonSdkError.fromHttp(response.status, parsedBody);
40
+ const httpError = AdaxonSdkError.fromHttp(response.status, parsedBody);
41
+ this.logRequestFailed(path, httpError, response.status);
42
+ throw httpError;
31
43
  }
32
44
  if (parsedBody === null) {
33
- throw new AdaxonSdkError('HTTP_ERROR', 'Adaxon returned an empty or invalid JSON response.', {
45
+ const invalidJsonError = new AdaxonSdkError('HTTP_ERROR', 'Adaxon returned an empty or invalid JSON response.', {
34
46
  status: response.status
35
47
  });
48
+ this.logRequestFailed(path, invalidJsonError, response.status);
49
+ throw invalidJsonError;
36
50
  }
51
+ logSdkActivity({
52
+ eventName: 'api.request.succeeded',
53
+ level: 'debug',
54
+ metadata: {
55
+ path,
56
+ status: response.status,
57
+ batch_size: batchSize
58
+ }
59
+ });
37
60
  return parsedBody;
38
61
  } catch (error) {
39
62
  if (error instanceof AdaxonSdkError) {
40
63
  throw error;
41
64
  }
42
65
  if (error instanceof Error && error.name === 'AbortError') {
43
- throw new AdaxonSdkError('TIMEOUT', 'Adaxon request timed out.', {
66
+ const timeoutError = new AdaxonSdkError('TIMEOUT', 'Adaxon request timed out.', {
44
67
  cause: error
45
68
  });
69
+ logSdkActivity({
70
+ eventName: 'api.request.timeout',
71
+ level: 'warn',
72
+ metadata: {
73
+ path,
74
+ error_code: timeoutError.code
75
+ },
76
+ error: timeoutError
77
+ });
78
+ throw timeoutError;
46
79
  }
47
80
  const detail = AdaxonSdkError.messageOf(error, '');
48
- throw new AdaxonSdkError('NETWORK_ERROR', `Unable to reach Adaxon at ${BASE_URL}.${detail ? ` ${detail}` : ''}`, {
81
+ const networkError = new AdaxonSdkError('NETWORK_ERROR', `Unable to reach Adaxon at ${BASE_URL}.${detail ? ` ${detail}` : ''}`, {
49
82
  cause: error
50
83
  });
84
+ this.logRequestFailed(path, networkError);
85
+ throw networkError;
51
86
  } finally {
52
87
  clearTimeout(timeout);
53
88
  }
54
89
  }
90
+ logRequestFailed(path, error, status) {
91
+ logSdkActivity({
92
+ eventName: 'api.request.failed',
93
+ level: 'warn',
94
+ metadata: {
95
+ path,
96
+ error_code: error.code,
97
+ ...(status !== undefined ? {
98
+ status
99
+ } : {})
100
+ },
101
+ error
102
+ });
103
+ }
55
104
  parseJson(rawBody) {
56
105
  try {
57
106
  return rawBody ? JSON.parse(rawBody) : null;
@@ -1,3 +1,3 @@
1
1
  /** Keep in sync with package.json "version" (published npm packages ship lib/ only). */
2
- export declare const SDK_VERSION = "0.1.13";
2
+ export declare const SDK_VERSION = "0.1.14";
3
3
  //# sourceMappingURL=sdkVersion.d.ts.map
@@ -0,0 +1,30 @@
1
+ import type { AdaxonPlatform } from '../../types/EventTypes.js';
2
+ export type AdaxonLogLevel = 'debug' | 'info' | 'warn' | 'error';
3
+ export type AdaxonActivityLogInput = {
4
+ eventName: string;
5
+ level?: AdaxonLogLevel;
6
+ metadata?: Record<string, unknown>;
7
+ error?: unknown;
8
+ /** Send a standalone Sentry issue (message or exception). Use sparingly. */
9
+ captureAsEvent?: boolean;
10
+ };
11
+ export type AdaxonLoggerConfigureInput = {
12
+ enabled: boolean;
13
+ dsn?: string;
14
+ platform: AdaxonPlatform;
15
+ testMode?: boolean;
16
+ };
17
+ declare class AdaxonLoggerService {
18
+ private enabled;
19
+ private platform;
20
+ private sentryReady;
21
+ configure(input: AdaxonLoggerConfigureInput): void;
22
+ reset(): void;
23
+ logActivity(input: AdaxonActivityLogInput): void;
24
+ private ensureSentry;
25
+ private safe;
26
+ }
27
+ export declare const AdaxonLogger: AdaxonLoggerService;
28
+ export declare function logSdkActivity(input: AdaxonActivityLogInput): void;
29
+ export {};
30
+ //# sourceMappingURL=AdaxonLogger.d.ts.map
@@ -0,0 +1,3 @@
1
+ /** Redact secrets and cap string size before activity logs leave the SDK. */
2
+ export declare function sanitizeLogMetadata(metadata: Record<string, unknown> | undefined): Record<string, unknown>;
3
+ //# sourceMappingURL=sanitizeLogMetadata.d.ts.map
@@ -8,6 +8,7 @@ export declare class ApiClient {
8
8
  private readonly fetchImplementation;
9
9
  constructor(config: ApiClientConfig);
10
10
  post<TResponse>(path: string, body: unknown): Promise<TResponse>;
11
+ private logRequestFailed;
11
12
  private parseJson;
12
13
  }
13
14
  export {};
@@ -10,6 +10,10 @@ export interface AdaxonSdkConfig {
10
10
  timeoutMs?: number;
11
11
  fetch?: typeof globalThis.fetch;
12
12
  testMode?: boolean;
13
+ /** When true, records SDK activity to Sentry for testing/debugging. Default false. */
14
+ activityLogging?: boolean;
15
+ /** Sentry DSN used when activity logging is enabled and the host app has not initialized Sentry. */
16
+ sentryDsn?: string;
13
17
  }
14
18
  export type AdaxonInitializationConfiguration = AdaxonSdkConfig;
15
19
  export type AdaxonInitializationErrorCode = 'INVALID_CONFIG' | 'UNKNOWN';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-test-adx",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Adaxon React Native / Expo SDK for event tracking and offer recommendations.",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -56,6 +56,9 @@
56
56
  "access": "public",
57
57
  "registry": "https://registry.npmjs.org/"
58
58
  },
59
+ "dependencies": {
60
+ "@sentry/react-native": "^8.31.0"
61
+ },
59
62
  "devDependencies": {
60
63
  "@eslint/compat": "^2.1.0",
61
64
  "@eslint/eslintrc": "^3.3.5",