@jolibox/implement 1.1.11-beta.3 → 1.1.11-beta.4

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.
@@ -4,6 +4,7 @@ import type { Track } from '../report';
4
4
  export type AdsDisplayPermission =
5
5
  | 'BLOCK_INITIAL'
6
6
  | 'BANNED_FOR_SESSION'
7
+ | 'HOLDING_HIGH_FREQ'
7
8
  | 'NETWORK_NOT_OK'
8
9
  | 'WAITING_BANNED_RELEASE'
9
10
  | 'BANNED_FOR_TIME'
@@ -13,35 +14,21 @@ interface CallAdsHistory {
13
14
  type: 'reward' | 'interstitial';
14
15
  }
15
16
 
16
- const TIMESTAMP_STORAGE_KEY = 'jolibox-sdk-ads-callbreak-timestamps';
17
-
18
- interface IAdsAntiCheatingConfig {
17
+ export interface IAdsAntiCheatingConfig {
19
18
  maxAllowedAdsForTime?: number; // default 8
20
- bannedForTimeThreshold?: number; // default 60000 (1 minute)
21
- bannedReleaseTime?: number; // default 60000 (1 minute), must be >= checkingTimeThreashold
19
+ banForTimeThreshold?: number; // default 60000 (1 minute)
20
+ bannedReleaseTime?: number; // default 60000 (1 minute), must be >= checkingTimeThreshold
22
21
  initialThreshold?: number; // default 2000
23
22
  maxAllowedAdsForSession?: number; // default 200
24
- bannedForSessionThreshold?: number; // default 600000 (10 minutes)
23
+ banForSessionThreshold?: number; // default 600000 (10 minutes)
24
+ highFreqThreshold?: number; // default 2000 (2s)
25
25
  }
26
26
 
27
27
  export class AdsAntiCheating {
28
- private maxAllowedAdsForTime = 8;
29
- private bannedForTimeThreshold = 60000;
30
- private bannedReleaseTime = 60000;
31
- private isBanningForTime = false;
32
- private releaseBannedForTimeTimeout: number | null = null;
33
-
34
- private maxAllowedAdsForSession = 200;
35
- private bannedForSessionThreshold = 600000;
36
- private isBanningForSession = false;
37
-
38
- private initialThreshold = 2000;
39
-
40
- private _callAdsTimestampsForTime: CallAdsHistory[] = []; // max x timestamps for time
41
- private callAdsTimestampsForSession: CallAdsHistory[] = []; // max x timestamps for session
42
-
43
- private initTimestamp = 0;
44
- // private context: JoliboxSDKPipeExecutor;
28
+ private checkShouldBanInitial: ReturnType<typeof createInitialStrategy>;
29
+ private checkShouldBanHighFreq: ReturnType<typeof createHighFreqStrategy>;
30
+ private checkShouldBanForTime: ReturnType<typeof createBanForTimeStrategy>;
31
+ private checkShouldBanForSession: ReturnType<typeof createBanForSessionStrategy>;
45
32
 
46
33
  constructor(
47
34
  readonly track: Track,
@@ -49,217 +36,296 @@ export class AdsAntiCheating {
49
36
  readonly checkNetwork: () => boolean,
50
37
  config?: IAdsAntiCheatingConfig
51
38
  ) {
52
- this.maxAllowedAdsForTime = config?.maxAllowedAdsForTime ?? 8;
53
- this.bannedForTimeThreshold = config?.bannedForTimeThreshold ?? 60000;
54
- this.bannedReleaseTime = config?.bannedReleaseTime ?? 60000;
55
-
56
- this.maxAllowedAdsForSession = config?.maxAllowedAdsForSession ?? 200;
57
- this.bannedForSessionThreshold = config?.bannedForSessionThreshold ?? 600000;
58
-
59
- this.initialThreshold = config?.initialThreshold ?? 2000;
39
+ const maxAllowedAdsForTime = config?.maxAllowedAdsForTime ?? 8;
40
+ const banForTimeThreshold = config?.banForTimeThreshold ?? 60000;
41
+ const bannedReleaseTime = config?.bannedReleaseTime ?? 60000;
42
+
43
+ const maxAllowedAdsForSession = config?.maxAllowedAdsForSession ?? 200;
44
+ const banForSessionThreshold = config?.banForSessionThreshold ?? 600000;
45
+
46
+ const initialThreshold = config?.initialThreshold ?? 2000;
47
+ const highFreqThreshold = config?.highFreqThreshold ?? 2000;
48
+
49
+ this.checkShouldBanInitial = createInitialStrategy(initialThreshold);
50
+ this.checkShouldBanHighFreq = createHighFreqStrategy(highFreqThreshold);
51
+ this.checkShouldBanForTime = createBanForTimeStrategy(
52
+ maxAllowedAdsForTime,
53
+ banForTimeThreshold,
54
+ bannedReleaseTime
55
+ );
56
+ this.checkShouldBanForSession = createBanForSessionStrategy(
57
+ maxAllowedAdsForSession,
58
+ banForSessionThreshold
59
+ );
60
+ }
60
61
 
61
- if (this.maxAllowedAdsForTime <= 1) {
62
- throw new Error('maxAllowedAdsForTime must be greater than 1');
62
+ public checkAdsDisplayPermission: (type: 'reward' | 'interstitial') => {
63
+ reason: AdsDisplayPermission;
64
+ info?: { count: number };
65
+ } = (type) => {
66
+ if (!this.checkNetwork()) {
67
+ return { reason: 'NETWORK_NOT_OK' };
63
68
  }
64
69
 
65
- if (this.bannedForTimeThreshold < 0) {
66
- throw new Error('bannedForTimeThreshold must be greater than or equal to 0');
70
+ const shouldBanInitial = this.checkShouldBanInitial();
71
+ if (shouldBanInitial) {
72
+ return { reason: shouldBanInitial };
67
73
  }
68
74
 
69
- if (this.bannedReleaseTime < this.bannedForTimeThreshold) {
70
- throw new Error('bannedReleaseTime must be greater than or equal to bannedForTimeThreshold');
75
+ const shouldBanForSession = this.checkShouldBanForSession(type);
76
+ if (shouldBanForSession) {
77
+ return { reason: shouldBanForSession };
71
78
  }
72
79
 
73
- if (this.initialThreshold < 0) {
74
- throw new Error('initialThreshold must be greater than or equal to 0');
80
+ const shouldBanForHighFreq = this.checkShouldBanHighFreq(type);
81
+ if (shouldBanForHighFreq) {
82
+ return shouldBanForHighFreq;
75
83
  }
76
84
 
77
- this.initCallAdsTimestampsForTime();
78
- this.initTimestamp = Date.now();
79
- // this.context = context;
80
- }
81
-
82
- /**
83
- * Restore call ads timestamps for time from local storage
84
- */
85
- private initCallAdsTimestampsForTime = () => {
86
- try {
87
- const fromStorage = JSON.parse(window.localStorage.getItem(TIMESTAMP_STORAGE_KEY) ?? '[]');
88
- if (Array.isArray(fromStorage)) {
89
- this._callAdsTimestampsForTime = fromStorage;
90
- } else {
91
- this._callAdsTimestampsForTime = [];
92
- }
93
- } catch {
94
- this._callAdsTimestampsForTime = [];
85
+ const shouldBanForTime = this.checkShouldBanForTime(type);
86
+ if (shouldBanForTime) {
87
+ return { reason: shouldBanForTime };
95
88
  }
96
- };
97
89
 
98
- /**
99
- * Get call ads timestamps for time
100
- */
101
- private get callAdsTimestampsForTime() {
102
- return this._callAdsTimestampsForTime;
103
- }
90
+ return { reason: 'ALLOWED' };
91
+ };
104
92
 
105
93
  /**
106
- * Set call ads timestamps for time, this will also save to local storage
94
+ * Report an ad display permission issue.
95
+ * Report to both event tracking system and the business backend system.
96
+ * @param reason
107
97
  */
108
- private set callAdsTimestampsForTime(timestamps: CallAdsHistory[]) {
109
- try {
110
- window.localStorage.setItem(TIMESTAMP_STORAGE_KEY, JSON.stringify(timestamps));
111
- } catch {
112
- console.error('Failed to save timestamps');
113
- }
114
- this._callAdsTimestampsForTime = timestamps;
98
+ public report(reason: AdsDisplayPermission, count?: number) {
99
+ const info = count ? { reason, count } : { reason };
100
+ this.track('PreventAdsCheating', info);
101
+ this.httpClient.post('/api/base/app-event', {
102
+ data: {
103
+ eventType: 'PREVENT_ADS_CHEATING',
104
+ adsInfo: info
105
+ }
106
+ });
115
107
  }
108
+ }
116
109
 
117
- /**
118
- * Set a timeout to release the banned for time status
119
- */
120
- private setReleaseBannedForTime = () => {
121
- if (this.releaseBannedForTimeTimeout) {
122
- window.clearTimeout(this.releaseBannedForTimeTimeout);
123
- this.releaseBannedForTimeTimeout = null;
124
- }
125
- this.releaseBannedForTimeTimeout = window.setTimeout(() => {
126
- this.isBanningForTime = false;
127
- this.releaseBannedForTimeTimeout = null;
128
- }, this.bannedReleaseTime);
129
- };
130
-
131
- /**
132
- * Check if we should ban for initial call. By default, if user makes a call less than 2 seconds after the first call, block.
133
- * @returns
134
- */
135
- private checkShouldBannedInitial = () => {
136
- const now = Date.now();
137
- const diffFromInit = now - this.initTimestamp;
138
- // if initial call less than x seconds, block
139
- if (diffFromInit <= this.initialThreshold) {
140
- return 'BLOCK_INITIAL';
141
- }
142
- };
110
+ const createBanForSessionStrategy = (maxAllowedAdsForSession: number, banForSessionThreshold: number) => {
111
+ let isBannedForSession = false;
112
+ let callAdsTimestampsForSession: CallAdsHistory[] = [];
143
113
 
144
114
  /**
145
115
  * Check if we should ban this user for the whole session. By default, if user makes more than 200 calls in 10 minutes, ban for the session.
146
116
  * @param type
147
117
  * @returns
148
118
  */
149
- private checkShouldBannedForSession = (type: 'reward' | 'interstitial') => {
150
- if (this.isBanningForSession) {
119
+ const check = (type: 'reward' | 'interstitial') => {
120
+ if (isBannedForSession) {
151
121
  return 'BANNED_FOR_SESSION';
152
122
  }
153
123
 
154
124
  const now = Date.now();
155
125
 
156
126
  // keep track of call timestamps
157
- this.callAdsTimestampsForSession = this.callAdsTimestampsForSession.concat({
127
+ callAdsTimestampsForSession = callAdsTimestampsForSession.concat({
158
128
  timestamp: now,
159
129
  type
160
130
  });
161
131
 
162
132
  // if only 1 call, no need to check
163
- if (this.callAdsTimestampsForSession.length === 1) {
133
+ if (callAdsTimestampsForSession.length === 1) {
164
134
  return undefined;
165
135
  }
166
136
 
167
137
  // keep last x timestamps
168
- if (this.callAdsTimestampsForSession.length > this.maxAllowedAdsForSession) {
169
- this.callAdsTimestampsForSession = this.callAdsTimestampsForSession.slice(
170
- -this.maxAllowedAdsForSession
171
- );
138
+ if (callAdsTimestampsForSession.length > maxAllowedAdsForSession) {
139
+ callAdsTimestampsForSession = callAdsTimestampsForSession.slice(-maxAllowedAdsForSession);
172
140
  }
173
141
 
174
142
  // if less than x seconds, ban for session
175
- if (this.callAdsTimestampsForSession.length === this.maxAllowedAdsForSession) {
176
- const diffFromFirst = now - this.callAdsTimestampsForSession[0].timestamp;
177
- if (diffFromFirst <= this.bannedForSessionThreshold) {
178
- this.isBanningForSession = true;
143
+ if (callAdsTimestampsForSession.length === maxAllowedAdsForSession) {
144
+ const diffFromFirst = now - callAdsTimestampsForSession[0].timestamp;
145
+ if (diffFromFirst <= banForSessionThreshold) {
146
+ isBannedForSession = true;
179
147
  return 'BANNED_FOR_SESSION';
180
148
  }
181
149
  }
182
150
  };
183
151
 
152
+ return check;
153
+ };
154
+
155
+ const createBanForTimeStrategy = (
156
+ maxAllowedAdsForTime: number,
157
+ banForTimeThreshold: number,
158
+ bannedReleaseTime: number
159
+ ) => {
160
+ let releaseBannedForTimeTimeout: number | null = null;
161
+
162
+ const setReleaseBannedForTime = (timeout: number) => {
163
+ if (releaseBannedForTimeTimeout) {
164
+ window.clearTimeout(releaseBannedForTimeTimeout);
165
+ releaseBannedForTimeTimeout = null;
166
+ }
167
+ releaseBannedForTimeTimeout = window.setTimeout(() => {
168
+ isBannedForTime.value = false;
169
+ releaseBannedForTimeTimeout = null;
170
+ }, timeout);
171
+ };
172
+
173
+ const TIMESTAMP_STORAGE_KEY = 'jolibox-sdk-ads-callbreak-timestamps';
174
+ const BAN_FOR_TIME_KEY = 'jolibox-sdk-ads-ban-for-time';
175
+
176
+ const callAdsHistory = {
177
+ _records: [] as CallAdsHistory[],
178
+ init() {
179
+ try {
180
+ const fromStorage = JSON.parse(window.localStorage.getItem(TIMESTAMP_STORAGE_KEY) ?? '[]');
181
+ if (Array.isArray(fromStorage)) {
182
+ this._records = fromStorage;
183
+ } else {
184
+ this._records = [];
185
+ }
186
+ } catch {
187
+ this._records = [];
188
+ }
189
+ },
190
+ get values() {
191
+ return this._records;
192
+ },
193
+ set values(value: CallAdsHistory[]) {
194
+ try {
195
+ window.localStorage.setItem(TIMESTAMP_STORAGE_KEY, JSON.stringify(value));
196
+ } catch {
197
+ // console.error('Failed to save timestamps');
198
+ // do nothing
199
+ }
200
+ this._records = value;
201
+ }
202
+ };
203
+
204
+ const isBannedForTime = {
205
+ _record: false,
206
+ init() {
207
+ try {
208
+ const fromStorage = JSON.parse(window.localStorage.getItem(BAN_FOR_TIME_KEY) ?? 'false');
209
+ if (typeof fromStorage === 'boolean') {
210
+ this._record = fromStorage;
211
+ }
212
+ } catch {
213
+ this._record = false; // default value
214
+ }
215
+ if (this._record) {
216
+ const lastCall: CallAdsHistory | undefined = callAdsHistory.values[callAdsHistory.values.length - 1];
217
+ const timeDiff = lastCall ? Date.now() - lastCall.timestamp : bannedReleaseTime;
218
+ if (timeDiff <= bannedReleaseTime && timeDiff > 0) {
219
+ // within the banned release time window
220
+ setReleaseBannedForTime(timeDiff);
221
+ } else {
222
+ this._record = false;
223
+ }
224
+ }
225
+ },
226
+ get value() {
227
+ return this._record;
228
+ },
229
+ set value(value: boolean) {
230
+ try {
231
+ window.localStorage.setItem(BAN_FOR_TIME_KEY, JSON.stringify(value));
232
+ } catch {
233
+ // do nothing
234
+ }
235
+ this._record = value;
236
+ }
237
+ };
238
+
239
+ callAdsHistory.init();
240
+ isBannedForTime.init();
241
+
184
242
  /**
185
243
  * Check if we should ban this user for a period of time. By default, if user makes more than 8 calls in 1 minute, ban for 1 minute.
186
244
  * After 1 minute, user can make calls again.
187
245
  * @param type
188
246
  * @returns
189
247
  */
190
- private checkShouldBannedForTime = (type: 'reward' | 'interstitial') => {
191
- if (this.isBanningForTime) {
248
+ const check = (type: 'reward' | 'interstitial') => {
249
+ if (isBannedForTime.value) {
192
250
  return 'WAITING_BANNED_RELEASE';
193
251
  }
194
252
 
195
253
  const now = Date.now();
196
254
 
197
255
  // keep track of call timestamps
198
- this.callAdsTimestampsForTime = this.callAdsTimestampsForTime.concat({
256
+ callAdsHistory.values = callAdsHistory.values.concat({
199
257
  timestamp: now,
200
258
  type
201
259
  });
202
260
 
203
261
  // if only 1 call, no need to check
204
- if (this.callAdsTimestampsForTime.length === 1) {
262
+ if (callAdsHistory.values.length === 1) {
205
263
  return undefined;
206
264
  }
207
265
 
208
266
  // keep last x timestamps
209
- if (this.callAdsTimestampsForTime.length > this.maxAllowedAdsForTime) {
210
- this.callAdsTimestampsForTime = this.callAdsTimestampsForTime.slice(-this.maxAllowedAdsForTime);
267
+ if (callAdsHistory.values.length > maxAllowedAdsForTime) {
268
+ callAdsHistory.values = callAdsHistory.values.slice(-maxAllowedAdsForTime);
211
269
  }
212
270
 
213
271
  // if less than x seconds, ban for time
214
- if (this.callAdsTimestampsForTime.length === this.maxAllowedAdsForTime) {
215
- const diffFromFirst = now - this.callAdsTimestampsForTime[0].timestamp;
216
- if (diffFromFirst <= this.bannedForTimeThreshold) {
217
- this.isBanningForTime = true;
218
- this.setReleaseBannedForTime();
272
+ if (callAdsHistory.values.length === maxAllowedAdsForTime) {
273
+ const diffFromFirst = now - callAdsHistory.values[0].timestamp;
274
+ if (diffFromFirst <= banForTimeThreshold) {
275
+ isBannedForTime.value = true;
276
+ setReleaseBannedForTime(bannedReleaseTime);
219
277
  return 'BANNED_FOR_TIME';
220
278
  }
221
279
  }
222
280
  };
281
+ return check;
282
+ };
223
283
 
224
- public checkAdsDisplayPermission: (type: 'reward' | 'interstitial') => AdsDisplayPermission = (type) => {
225
- if (!this.checkNetwork()) {
226
- return 'NETWORK_NOT_OK';
227
- }
228
-
229
- const shouldBannedInitial = this.checkShouldBannedInitial();
230
- if (shouldBannedInitial) {
231
- return shouldBannedInitial;
232
- }
233
-
234
- const shouldBannedForSession = this.checkShouldBannedForSession(type);
235
- if (shouldBannedForSession) {
236
- return shouldBannedForSession;
237
- }
284
+ const createInitialStrategy = (initialThreshold: number) => {
285
+ const initTimestamp = Date.now();
238
286
 
239
- const shouldBannedForTime = this.checkShouldBannedForTime(type);
240
- if (shouldBannedForTime) {
241
- return shouldBannedForTime;
287
+ /**
288
+ * Check if we should ban for initial call. By default, if user makes a call less than 2 seconds after the first call, block.
289
+ * @returns
290
+ */
291
+ const check = () => {
292
+ const now = Date.now();
293
+ const diffFromInit = now - initTimestamp;
294
+ // if initial call less than x seconds, block
295
+ if (diffFromInit <= initialThreshold) {
296
+ return 'BLOCK_INITIAL';
242
297
  }
243
-
244
- return 'ALLOWED';
245
298
  };
299
+ return check;
300
+ };
301
+
302
+ const createHighFreqStrategy = (highFreqThreshold = 2000) => {
303
+ let highFreqCounter = 0;
304
+ let highFreqTimer: number | null = null;
246
305
 
247
306
  /**
248
- * Report an ad display permission issue.
249
- * Report to both event tracking system and the business backend system.
250
- * @param reason
307
+ * Check if we should ban for high frequency calling. If user makes a call more than 1 times in x seconds, block.
308
+ * This only works for reward ads.
309
+ *
310
+ * @param type
311
+ * @returns
251
312
  */
252
- public report(reason: AdsDisplayPermission) {
253
- this.track('PreventAdsCheating', {
254
- reason
255
- });
256
- this.httpClient.post('/api/base/app-event', {
257
- data: {
258
- eventType: 'PREVENT_ADS_CHEATING',
259
- adsInfo: {
260
- reason
261
- }
313
+ const check = (type: 'reward' | 'interstitial') => {
314
+ if (type === 'reward') {
315
+ if (highFreqTimer) {
316
+ highFreqCounter += 1;
317
+ return { reason: 'HOLDING_HIGH_FREQ' as const, info: { count: highFreqCounter } };
318
+ } else {
319
+ highFreqTimer = window.setTimeout(() => {
320
+ if (highFreqTimer) {
321
+ window.clearTimeout(highFreqTimer);
322
+ highFreqTimer = 0;
323
+ highFreqCounter = 0;
324
+ }
325
+ }, highFreqThreshold);
262
326
  }
263
- });
264
- }
265
- }
327
+ }
328
+ };
329
+
330
+ return check;
331
+ };
@@ -481,32 +481,42 @@ export class JoliboxAdsImpl {
481
481
  }
482
482
  }
483
483
 
484
- const adsDisplayPermission = this.antiCheating.checkAdsDisplayPermission(
484
+ const { reason, info } = this.antiCheating.checkAdsDisplayPermission(
485
485
  params.type === 'reward' ? 'reward' : 'interstitial'
486
486
  );
487
487
 
488
- switch (adsDisplayPermission) {
488
+ switch (reason) {
489
489
  case 'NETWORK_NOT_OK':
490
490
  case 'BANNED_FOR_SESSION':
491
- console.warn('Ads not allowed', adsDisplayPermission);
491
+ // console.warn('Ads not allowed', reason);
492
492
  params.adBreakDone?.({
493
493
  breakType: params.type,
494
- breakName: adsDisplayPermission,
494
+ breakName: reason,
495
495
  breakFormat: params.type === 'reward' ? 'reward' : 'interstitial',
496
496
  breakStatus: 'noAdPreloaded'
497
497
  });
498
- this.antiCheating.report(adsDisplayPermission);
498
+ this.antiCheating.report(reason);
499
499
  return;
500
500
  case 'BLOCK_INITIAL':
501
501
  case 'BANNED_FOR_TIME':
502
502
  case 'WAITING_BANNED_RELEASE':
503
- console.warn('Ads not allowed', adsDisplayPermission);
503
+ // console.warn('Ads not allowed', adsDisplayPermission);
504
504
  params.adBreakDone?.({
505
505
  breakType: params.type,
506
506
  breakFormat: params.type === 'reward' ? 'reward' : 'interstitial',
507
507
  breakStatus: 'frequencyCapped'
508
508
  });
509
- this.antiCheating.report(adsDisplayPermission);
509
+ this.antiCheating.report(reason);
510
+ return;
511
+ case 'HOLDING_HIGH_FREQ':
512
+ params.adBreakDone?.({
513
+ breakType: params.type,
514
+ breakFormat: params.type === 'reward' ? 'reward' : 'interstitial',
515
+ breakStatus: 'frequencyCapped'
516
+ });
517
+ if ((info?.count ?? 0) === 3) {
518
+ this.antiCheating.report(reason, info?.count);
519
+ }
510
520
  return;
511
521
  default:
512
522
  // allowed