@chipmobilesdk/rn-ads 0.2.1 → 0.4.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.
@@ -34,6 +34,7 @@ export const INITIAL_CONSENT: ConsentSnapshot = {
34
34
  state: 'undetermined',
35
35
  canRequestAds: false,
36
36
  isFormAvailable: false,
37
+ privacyOptionsRequirement: 'unknown',
37
38
  error: null,
38
39
  };
39
40
 
@@ -59,6 +60,7 @@ export function normalizeConsent(info: EngineConsentInfo): ConsentSnapshot {
59
60
  state,
60
61
  canRequestAds: info.canRequestAds === true,
61
62
  isFormAvailable: info.isFormAvailable === true,
63
+ privacyOptionsRequirement: info.privacyOptions,
62
64
  error: null,
63
65
  };
64
66
  }
@@ -70,9 +72,19 @@ export function normalizeConsent(info: EngineConsentInfo): ConsentSnapshot {
70
72
  * There is no failure mode in which "the flow broke" should mean "go ahead", and
71
73
  * making that structural rather than conditional means no future edit can
72
74
  * accidentally reintroduce the inference.
75
+ *
76
+ * The privacy-options requirement is `unknown` for the same reason: a broken
77
+ * flow has no provider verdict to forward, and reporting `not_required` would be
78
+ * the package deciding the user has no right to change their mind (FR-150).
73
79
  */
74
80
  export function consentFailure(error: AdsError): ConsentSnapshot {
75
- return { state: 'error', canRequestAds: false, isFormAvailable: false, error };
81
+ return {
82
+ state: 'error',
83
+ canRequestAds: false,
84
+ isFormAvailable: false,
85
+ privacyOptionsRequirement: 'unknown',
86
+ error,
87
+ };
76
88
  }
77
89
 
78
90
  export function toConsentError(cause: unknown, message: string): AdsError {
@@ -161,6 +173,7 @@ export function createConsentPortGuard(port: ConsentPort | undefined): ConsentPo
161
173
  return {
162
174
  requestInfoUpdate: unavailable,
163
175
  showFormIfRequired: unavailable,
176
+ showPrivacyOptionsForm: unavailable,
164
177
  getInfo: unavailable,
165
178
  reset: () => undefined,
166
179
  };
@@ -23,6 +23,13 @@ export type InitEvent =
23
23
  | { readonly type: 'UNDECIDE' }
24
24
  | { readonly type: 'ENABLE' }
25
25
  | { readonly type: 'SUCCEED' }
26
+ /**
27
+ * Back to `ready` with no native call, because the provider was initialized
28
+ * earlier and nothing has torn it down since (FR-154). The instance decides
29
+ * whether that holds; this machine only refuses to resume from a state a
30
+ * fresh start could not leave either.
31
+ */
32
+ | { readonly type: 'RESUME' }
26
33
  | { readonly type: 'FAIL'; readonly error: AdsError };
27
34
 
28
35
  export interface InitSnapshot {
@@ -165,6 +172,12 @@ export function initReducer(state: InitSnapshot, event: InitEvent): InitSnapshot
165
172
  // resurrect a state the app deliberately left.
166
173
  return state.status === 'initializing' ? at('ready') : state;
167
174
 
175
+ case 'RESUME':
176
+ // Same guard as START, for the same reason: `disabled`, `undecided` and
177
+ // `unavailable` are not left by a gate reopening. `canStart` excludes
178
+ // them, and `initializing`, whose own outcome is still on its way.
179
+ return canStart(state.status) ? at('ready') : state;
180
+
168
181
  case 'FAIL':
169
182
  return state.status === 'initializing' ? at('failed', { error: event.error }) : state;
170
183
 
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Per-placement request throttle (FR-140).
3
+ *
4
+ * This exists to protect the **ad account**, not to shape product behaviour, and
5
+ * that distinction is the whole reason it lives in the package:
6
+ *
7
+ * | Decision | Owner |
8
+ * |---|---|
9
+ * | How often to *show* an interstitial | the app — product policy |
10
+ * | How often the same placement may *request* | the package — account safety |
11
+ *
12
+ * Rapid repeat requests for one placement are read by ad networks as invalid
13
+ * traffic, and the penalty is account-level and not recoverable by shipping a
14
+ * fix. That puts it in the same class as the test-ad substitution the package
15
+ * already enforces: a safety property the library guarantees rather than
16
+ * documents and hopes each app remembers.
17
+ *
18
+ * ## The pattern it is built for
19
+ *
20
+ * A banner mounted inside a view the user navigates between — a day in a
21
+ * calendar, a tab, a row in a list — is unmounted and remounted by React on every
22
+ * navigation. Each remount is a fresh provider request. A user swiping quickly
23
+ * through ten days produces ten requests in a few seconds, and nothing about that
24
+ * is the app author doing something wrong.
25
+ *
26
+ * Full-screen formats do not have this problem: they are loaded by an explicit
27
+ * call, not mounted by a renderer. That asymmetry is why the default differs by
28
+ * format — see `defaultMinRequestIntervalMs`.
29
+ *
30
+ * The better *architectural* answer is to keep the banner outside the subtree that
31
+ * remounts. This throttle is the safety net for when that is not possible, not a
32
+ * licence to ignore the problem; the README says so where an app author will read
33
+ * it.
34
+ */
35
+
36
+ import type { AdFormat } from '../config/types';
37
+
38
+ /**
39
+ * AdMob's own documented floor for banner auto-refresh is 60 seconds, so
40
+ * requesting the same banner placement more often than that is, by the provider's
41
+ * own standard, more often than it considers reasonable. Taking the provider's
42
+ * number rather than inventing one means the default is defensible rather than
43
+ * arbitrary.
44
+ */
45
+ export const DEFAULT_BANNER_MIN_REQUEST_INTERVAL_MS = 60_000;
46
+
47
+ /**
48
+ * Off by default for full-screen formats.
49
+ *
50
+ * `load` → `show` → `load` is the normal, healthy cycle for an interstitial, and
51
+ * an app that sells three things in two minutes legitimately loads three times.
52
+ * Throttling that by default would break correct apps to solve a problem those
53
+ * formats do not have. An app that wants a floor here can set one.
54
+ */
55
+ export const DEFAULT_FULL_SCREEN_MIN_REQUEST_INTERVAL_MS = 0;
56
+
57
+ export const MIN_REQUEST_INTERVAL_BOUNDS = { min: 0, max: 600_000 } as const;
58
+
59
+ /** The format-aware default, applied when a placement configures nothing. */
60
+ export function defaultMinRequestIntervalMs(format: AdFormat): number {
61
+ return format === 'banner'
62
+ ? DEFAULT_BANNER_MIN_REQUEST_INTERVAL_MS
63
+ : DEFAULT_FULL_SCREEN_MIN_REQUEST_INTERVAL_MS;
64
+ }
65
+
66
+ export type ThrottleDecision =
67
+ | { readonly allowed: true }
68
+ /**
69
+ * `retryAfterMs` is reported rather than left for the caller to compute,
70
+ * because the app needs it to decide between collapsing the space and holding
71
+ * it — and a caller recomputing it from a timestamp would drift from the
72
+ * clock this throttle actually used.
73
+ */
74
+ | { readonly allowed: false; readonly retryAfterMs: number };
75
+
76
+ export interface RequestThrottle {
77
+ /**
78
+ * Records a request and reports whether it may proceed.
79
+ *
80
+ * Call immediately before handing a request to the provider.
81
+ *
82
+ * `token` makes the acquisition **idempotent for one request cycle**, and the
83
+ * banner path depends on it. React may run a `useMemo` or an effect more than
84
+ * once for a single committed render — StrictMode does it deliberately — and
85
+ * without a token the second run would consume a second slot and be refused,
86
+ * so a banner would never appear in development. Passing the cycle's
87
+ * correlation id means "this is the same request I already asked about", and
88
+ * the previous decision is returned unchanged.
89
+ *
90
+ * Omit `token` for a genuinely new imperative request, where every call is a
91
+ * distinct intent to ask the network.
92
+ */
93
+ tryAcquire(key: string, minIntervalMs: number, token?: string): ThrottleDecision;
94
+ /**
95
+ * Forgets a placement's last request.
96
+ *
97
+ * Used when a placement's resources are released, so a genuinely new cycle is
98
+ * not charged for the previous one.
99
+ */
100
+ reset(key: string): void;
101
+ clear(): void;
102
+ /** Diagnostics only: when the placement last requested, or undefined. */
103
+ lastRequestAt(key: string): number | undefined;
104
+ }
105
+
106
+ export function createRequestThrottle(now: () => number): RequestThrottle {
107
+ const lastAt = new Map<string, number>();
108
+ /** The token and verdict of the most recent acquisition, for idempotency. */
109
+ const lastToken = new Map<string, { token: string; decision: ThrottleDecision }>();
110
+
111
+ const decide = (key: string, minIntervalMs: number): ThrottleDecision => {
112
+ // Zero is the documented way to turn the throttle off, so it must never
113
+ // consult the clock — an app that sets 0 should behave exactly as it did
114
+ // before this feature existed.
115
+ if (minIntervalMs <= 0) {
116
+ return { allowed: true };
117
+ }
118
+ const previous = lastAt.get(key);
119
+ if (previous === undefined) {
120
+ return { allowed: true };
121
+ }
122
+ const elapsed = now() - previous;
123
+ // A clock that moved backwards — a manual time change, or an NTP
124
+ // correction — must not lock a placement out until the clock catches up.
125
+ // Treating it as "enough time passed" fails open, which is right: the
126
+ // throttle protects against bursts, and a backwards clock is not a burst.
127
+ if (elapsed < 0) {
128
+ return { allowed: true };
129
+ }
130
+ if (elapsed >= minIntervalMs) {
131
+ return { allowed: true };
132
+ }
133
+ return { allowed: false, retryAfterMs: minIntervalMs - elapsed };
134
+ };
135
+
136
+ return {
137
+ tryAcquire(key, minIntervalMs, token) {
138
+ if (token !== undefined) {
139
+ const previous = lastToken.get(key);
140
+ if (previous?.token === token) {
141
+ // Same cycle asking again. Replaying the stored verdict keeps a
142
+ // double-invoked render from charging twice, and keeps a refused
143
+ // cycle refused rather than letting a retry slip through.
144
+ return previous.decision;
145
+ }
146
+ }
147
+
148
+ // Nothing is recorded when the throttle is off for this placement.
149
+ // Recording would be dead work, and it would also mean that turning the
150
+ // throttle on later immediately compared against a stale timestamp.
151
+ if (minIntervalMs <= 0) {
152
+ return { allowed: true };
153
+ }
154
+
155
+ const decision = decide(key, minIntervalMs);
156
+ if (decision.allowed) {
157
+ lastAt.set(key, now());
158
+ }
159
+ if (token !== undefined) {
160
+ lastToken.set(key, { token, decision });
161
+ }
162
+ return decision;
163
+ },
164
+ reset(key) {
165
+ lastAt.delete(key);
166
+ lastToken.delete(key);
167
+ },
168
+ clear() {
169
+ lastAt.clear();
170
+ lastToken.clear();
171
+ },
172
+ lastRequestAt(key) {
173
+ return lastAt.get(key);
174
+ },
175
+ };
176
+ }
@@ -33,6 +33,12 @@ export type AdEventType =
33
33
  // cycle that triggered it. Each attempt is a billable request, so it has to
34
34
  // be reconcilable against a bill from the event stream alone (FR-138).
35
35
  | 'retry_scheduled'
36
+ // Emitted when the per-placement throttle refuses a request (FR-140). Carries
37
+ // no correlation id because no cycle began: nothing was requested, so there is
38
+ // nothing to correlate. Present so a throttled placement is visible in
39
+ // whatever logging the app already has, rather than looking like a silent
40
+ // absence of ads.
41
+ | 'request_throttled'
36
42
  | 'internal_error';
37
43
 
38
44
  interface BaseEvent {
@@ -76,6 +82,13 @@ export type AdEvent = BaseEvent &
76
82
  readonly delayMs: number;
77
83
  readonly error: AdsError;
78
84
  }
85
+ | {
86
+ readonly type: 'request_throttled';
87
+ /** How long until this placement may request again. */
88
+ readonly retryAfterMs: number;
89
+ /** The configured floor that refused it, so the cause is readable. */
90
+ readonly minRequestIntervalMs: number;
91
+ }
79
92
  | {
80
93
  readonly type: 'config_warning';
81
94
  readonly warning: ConfigWarning;
@@ -61,6 +61,7 @@ interface ProviderModule {
61
61
  interface ProviderConsent {
62
62
  requestInfoUpdate(options?: Record<string, unknown>): Promise<ProviderConsentInfo>;
63
63
  loadAndShowConsentFormIfRequired(): Promise<ProviderConsentInfo>;
64
+ showPrivacyOptionsForm(): Promise<ProviderConsentInfo>;
64
65
  getConsentInfo(): Promise<ProviderConsentInfo>;
65
66
  reset(): void;
66
67
  }
@@ -69,6 +70,7 @@ interface ProviderConsentInfo {
69
70
  status?: string;
70
71
  canRequestAds?: boolean;
71
72
  isConsentFormAvailable?: boolean;
73
+ privacyOptionsRequirementStatus?: string;
72
74
  }
73
75
 
74
76
  /**
@@ -119,9 +121,25 @@ function toEngineConsentInfo(info: ProviderConsentInfo): EngineConsentInfo {
119
121
  status: normalized,
120
122
  canRequestAds: info.canRequestAds === true,
121
123
  isFormAvailable: info.isConsentFormAvailable === true,
124
+ privacyOptions: toEnginePrivacyOptions(info.privacyOptionsRequirementStatus),
122
125
  };
123
126
  }
124
127
 
128
+ /**
129
+ * Same rule as the status: an unrecognized value is `unknown`, never a guess in
130
+ * either direction (FR-150).
131
+ */
132
+ function toEnginePrivacyOptions(value: string | undefined): EngineConsentInfo['privacyOptions'] {
133
+ switch (String(value ?? 'UNKNOWN').toUpperCase()) {
134
+ case 'REQUIRED':
135
+ return 'required';
136
+ case 'NOT_REQUIRED':
137
+ return 'not_required';
138
+ default:
139
+ return 'unknown';
140
+ }
141
+ }
142
+
125
143
  function loadProvider(): ProviderModule | null {
126
144
  try {
127
145
  return require('react-native-google-mobile-ads') as ProviderModule;
@@ -362,6 +380,14 @@ function createAdMobConsent(getProvider: () => ProviderModule | null): ConsentPo
362
380
  return toEngineConsentInfo(info);
363
381
  },
364
382
 
383
+ async showPrivacyOptionsForm() {
384
+ // Present in the pinned bridge 16.0.3, so no range change (FR-155).
385
+ // Resolves with the consent information as it stands once the form
386
+ // closes, whether the user changed anything or not.
387
+ const info = await requireConsent().showPrivacyOptionsForm();
388
+ return toEngineConsentInfo(info);
389
+ },
390
+
365
391
  async getInfo() {
366
392
  return toEngineConsentInfo(await requireConsent().getConsentInfo());
367
393
  },
@@ -52,6 +52,8 @@ export interface EngineConsentInfo {
52
52
  readonly status: 'unknown' | 'required' | 'not_required' | 'obtained';
53
53
  readonly canRequestAds: boolean;
54
54
  readonly isFormAvailable: boolean;
55
+ /** Whether the provider requires a "change my choice" entry point (FR-150). */
56
+ readonly privacyOptions: 'unknown' | 'required' | 'not_required';
55
57
  }
56
58
 
57
59
  export interface EngineConsentOptions {
@@ -76,6 +78,13 @@ export interface ConsentPort {
76
78
  requestInfoUpdate(options: EngineConsentOptions): Promise<EngineConsentInfo>;
77
79
  /** Presents the provider's form if one is required; a no-op otherwise. */
78
80
  showFormIfRequired(): Promise<EngineConsentInfo>;
81
+ /**
82
+ * Presents the provider's privacy options form, through which a user changes
83
+ * an answer already given (FR-151). Resolves when the form closes, whether or
84
+ * not anything changed; rejects if it could not be presented. The caller has
85
+ * already checked the requirement, so the adapter does not re-check.
86
+ */
87
+ showPrivacyOptionsForm(): Promise<EngineConsentInfo>;
79
88
  /** Last known information without contacting the network. */
80
89
  getInfo(): Promise<EngineConsentInfo>;
81
90
  /** Clears the stored decision. Inert in production by configuration. */
package/src/index.ts CHANGED
@@ -1,91 +1,103 @@
1
- /**
2
- * @chipmobilesdk/rn-ads — reusable advertising capability for
3
- * ChipMobileSdk-family React Native apps.
4
- *
5
- * This is the public entrypoint (LIB-003). Modules under `config/`, `core/`,
6
- * `engine/`, `react/`, and `diagnostics/` are private; import them only through
7
- * this file. The fake engine lives behind `@chipmobilesdk/rn-ads/testing` so
8
- * test doubles cannot reach a production bundle.
9
- *
10
- * Nothing exported here names Google, AdMob, or any provider type (FR-002).
11
- * `src/__tests__/public-api.test.ts` asserts that mechanically.
12
- *
13
- * The package owns capability: a safe, consistent way to request and present
14
- * ads. The consuming app owns policy — where ads appear, when, how often, under
15
- * what consent, and whether they appear at all. There is no placement rule, no
16
- * frequency cap, and no screen name in this package (FR-045).
17
- */
18
-
19
- // Configuration and state
20
- export type {
21
- AdContentRating,
22
- AdFormat,
23
- AdsConfig,
24
- ConsentSnapshot,
25
- ConsentState,
26
- ConsentTestingOptions,
27
- Enablement,
28
- Environment,
29
- FullScreenFormat,
30
- InitStatus,
31
- PlacementDefinition,
32
- PlacementState,
33
- PrivacyBlockReason,
34
- PrivacyOptions,
35
- RetryPolicy,
36
- Reward,
37
- TimeoutConfig,
38
- UnavailableReason,
39
- Unsubscribe,
40
- } from './config/types';
41
- export { isFullScreenFormat } from './config/types';
42
-
43
- // Timeout policy — exported so a consumer can read the defaults it is bound by
44
- // rather than discovering them from a timeout in production (FR-032).
45
- export {
46
- AD_VALIDITY_BOUNDS,
47
- APP_OPEN_VALIDITY_BOUNDS,
48
- DEFAULT_AD_VALIDITY_MS,
49
- DEFAULT_APP_OPEN_VALIDITY_MS,
50
- DEFAULT_LOAD_TIMEOUT_MS,
51
- DEFAULT_RETRY_POLICY,
52
- DEFAULT_SHOW_TIMEOUT_MS,
53
- DEFAULT_UNDECIDED_WARNING_MS,
54
- LOAD_TIMEOUT_BOUNDS,
55
- RETRY_BOUNDS,
56
- SHOW_TIMEOUT_BOUNDS,
57
- UNDECIDED_WARNING_BOUNDS,
58
- } from './core/timers';
59
-
60
- // Enablement. `EnablementResult` is what `setEnabled` returns in place of
61
- // 0.1.0's `void` — the defect that let a re-enable fail in silence (FR-122).
62
- export type { EnablementResult } from './core/enablement';
63
-
64
- // Instance, lifecycle, and results
65
- export { createAds } from './core/adsInstance';
66
- export type {
67
- AdsInstance,
68
- ConsentApi,
69
- InitResult,
70
- LoadResult,
71
- PrivacyUpdateResult,
72
- ShowResult,
73
- } from './core/adsInstance';
74
-
75
- // React surface
76
- export { AdsBanner } from './react/AdsBanner';
77
- export type { AdsBannerProps } from './react/AdsBanner';
78
- export { AdsProvider, useAds } from './react/AdsProvider';
79
- export type { AdsProviderProps } from './react/AdsProvider';
80
- export { useAdsEvents, useAdsStatus, usePlacementState } from './react/hooks';
81
-
82
- // Diagnostics
83
- export { AdsConfigError, AdsError, isAdsError } from './diagnostics/errors';
84
- export type { AdsErrorCode, AdsOperation } from './diagnostics/errors';
85
- export type {
86
- AdEvent,
87
- AdEventListener,
88
- AdEventType,
89
- ConfigWarning,
90
- ImpressionSource,
91
- } from './diagnostics/events';
1
+ /**
2
+ * @chipmobilesdk/rn-ads — reusable advertising capability for
3
+ * ChipMobileSdk-family React Native apps.
4
+ *
5
+ * This is the public entrypoint (LIB-003). Modules under `config/`, `core/`,
6
+ * `engine/`, `react/`, and `diagnostics/` are private; import them only through
7
+ * this file. The fake engine lives behind `@chipmobilesdk/rn-ads/testing` so
8
+ * test doubles cannot reach a production bundle.
9
+ *
10
+ * Nothing exported here names Google, AdMob, or any provider type (FR-002).
11
+ * `src/__tests__/public-api.test.ts` asserts that mechanically.
12
+ *
13
+ * The package owns capability: a safe, consistent way to request and present
14
+ * ads. The consuming app owns policy — where ads appear, when, how often, under
15
+ * what consent, and whether they appear at all. There is no placement rule, no
16
+ * frequency cap, and no screen name in this package (FR-045).
17
+ */
18
+
19
+ // Configuration and state
20
+ export type {
21
+ AdContentRating,
22
+ AdFormat,
23
+ AdsConfig,
24
+ ConsentSnapshot,
25
+ ConsentState,
26
+ ConsentTestingOptions,
27
+ Enablement,
28
+ Environment,
29
+ FullScreenFormat,
30
+ InitStatus,
31
+ PlacementDefinition,
32
+ PlacementState,
33
+ PrivacyBlockReason,
34
+ PrivacyOptions,
35
+ PrivacyOptionsRequirement,
36
+ RetryPolicy,
37
+ Reward,
38
+ TimeoutConfig,
39
+ UnavailableReason,
40
+ Unsubscribe,
41
+ } from './config/types';
42
+ export { isFullScreenFormat } from './config/types';
43
+
44
+ // Timeout policy — exported so a consumer can read the defaults it is bound by
45
+ // rather than discovering them from a timeout in production (FR-032).
46
+ export {
47
+ AD_VALIDITY_BOUNDS,
48
+ APP_OPEN_VALIDITY_BOUNDS,
49
+ DEFAULT_AD_VALIDITY_MS,
50
+ DEFAULT_APP_OPEN_VALIDITY_MS,
51
+ DEFAULT_LOAD_TIMEOUT_MS,
52
+ DEFAULT_RETRY_POLICY,
53
+ DEFAULT_SHOW_TIMEOUT_MS,
54
+ DEFAULT_UNDECIDED_WARNING_MS,
55
+ LOAD_TIMEOUT_BOUNDS,
56
+ RETRY_BOUNDS,
57
+ SHOW_TIMEOUT_BOUNDS,
58
+ UNDECIDED_WARNING_BOUNDS,
59
+ } from './core/timers';
60
+
61
+ // Request throttle — account safety, added in 0.3.0 (FR-140). Exported so an app
62
+ // can read the floor it is bound by, and so the format-aware default is legible
63
+ // rather than something to discover from a `throttled` state in production.
64
+ export {
65
+ DEFAULT_BANNER_MIN_REQUEST_INTERVAL_MS,
66
+ DEFAULT_FULL_SCREEN_MIN_REQUEST_INTERVAL_MS,
67
+ MIN_REQUEST_INTERVAL_BOUNDS,
68
+ defaultMinRequestIntervalMs,
69
+ } from './core/requestThrottle';
70
+
71
+ // Enablement. `EnablementResult` is what `setEnabled` returns in place of
72
+ // 0.1.0's `void` — the defect that let a re-enable fail in silence (FR-122).
73
+ export type { EnablementResult } from './core/enablement';
74
+
75
+ // Instance, lifecycle, and results
76
+ export { createAds } from './core/adsInstance';
77
+ export type {
78
+ AdsInstance,
79
+ ConsentApi,
80
+ InitResult,
81
+ LoadResult,
82
+ PrivacyOptionsFormResult,
83
+ PrivacyUpdateResult,
84
+ ShowResult,
85
+ } from './core/adsInstance';
86
+
87
+ // React surface
88
+ export { AdsBanner } from './react/AdsBanner';
89
+ export type { AdsBannerProps } from './react/AdsBanner';
90
+ export { AdsProvider, useAds } from './react/AdsProvider';
91
+ export type { AdsProviderProps } from './react/AdsProvider';
92
+ export { useAdsEvents, useAdsStatus, usePlacementState } from './react/hooks';
93
+
94
+ // Diagnostics
95
+ export { AdsConfigError, AdsError, isAdsError } from './diagnostics/errors';
96
+ export type { AdsErrorCode, AdsOperation } from './diagnostics/errors';
97
+ export type {
98
+ AdEvent,
99
+ AdEventListener,
100
+ AdEventType,
101
+ ConfigWarning,
102
+ ImpressionSource,
103
+ } from './diagnostics/events';