@internetarchive/ia-book-actions 1.4.4-alpha9 → 1.4.6-alpha1
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/index.html +38 -1
- package/package.json +1 -1
- package/src/assets/data/info-icon.js +17 -0
- package/src/core/services/actions-handler/actions-handler-service.js +129 -9
- package/src/core/services/actions-handler/actions-handler.js +90 -38
- package/src/core/services/loan-analytics.js +0 -11
- package/src/core/services/loan-renew-helper.js +2 -2
- package/src/core/services/loan-token-poller.js +108 -22
- package/src/core/services/sentry-report.js +40 -0
- package/src/ia-book-actions.js +329 -250
- package/test/core/services/actions-handler-service.test.js +162 -0
- package/test/core/services/loan-token-poller.test.js +182 -15
- package/test/core/services/sentry-report.test.js +72 -0
- package/test/ia-book-actions.test.js +628 -35
- package/src/assets/data/info.js +0 -12
package/index.html
CHANGED
|
@@ -360,7 +360,33 @@
|
|
|
360
360
|
});
|
|
361
361
|
});
|
|
362
362
|
|
|
363
|
-
|
|
363
|
+
// WEBDEV-8322 repro: fail only create_token (via ?failAction=create_token),
|
|
364
|
+
// leaving renew_loan (and every other action) succeeding normally —
|
|
365
|
+
// see actions-handler-service.js's shouldReturnError.
|
|
366
|
+
document.querySelectorAll('.failCreateToken input[type=checkbox]').forEach((item) => {
|
|
367
|
+
if (window.location.href.indexOf('?failAction=create_token') !== -1) {
|
|
368
|
+
item.checked = true;
|
|
369
|
+
}
|
|
370
|
+
item.addEventListener('click', (e) => {
|
|
371
|
+
console.log(`[Demo] fail-create_token-only mode ${e.target.checked ? 'enabled' : 'disabled'}`);
|
|
372
|
+
|
|
373
|
+
var searchParams = new URLSearchParams(window.location.search);
|
|
374
|
+
var separate = '';
|
|
375
|
+
if (e.target.checked) {
|
|
376
|
+
separate = '?';
|
|
377
|
+
searchParams.set('failAction', 'create_token');
|
|
378
|
+
} else {
|
|
379
|
+
separate = '';
|
|
380
|
+
searchParams.delete('failAction');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (history.pushState) {
|
|
384
|
+
var newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + separate + searchParams;
|
|
385
|
+
window.history.pushState({path:newurl},'',newurl);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
364
390
|
document.querySelectorAll('.userState input[type=checkbox]').forEach((item) => {
|
|
365
391
|
item.addEventListener('click', (e) => {
|
|
366
392
|
if (e.target.checked) {
|
|
@@ -726,6 +752,17 @@
|
|
|
726
752
|
</span>
|
|
727
753
|
</td>
|
|
728
754
|
</tr>
|
|
755
|
+
<tr colspan='5'>
|
|
756
|
+
<td class="failCreateToken">
|
|
757
|
+
<label><input type="checkbox" name="failCreateToken" value="failCreateToken" />Fail create_token only (renew_loan still succeeds)</label>
|
|
758
|
+
<span style="color: #666; font-size: 11px; display: block; padding: 0 10px 10px;">
|
|
759
|
+
WEBDEV-8322 repro: unlike the checkbox above, this leaves renew_loan
|
|
760
|
+
succeeding and fails only the create_token call right after it — turn
|
|
761
|
+
this on, then run scenario 2 → 3 (or 2 → 4) below. Expect the error
|
|
762
|
+
modal, and the bar/timer resetting to Borrow/0 (no retry).
|
|
763
|
+
</span>
|
|
764
|
+
</td>
|
|
765
|
+
</tr>
|
|
729
766
|
<tr>
|
|
730
767
|
<td class="userState">
|
|
731
768
|
<label><input type="checkbox" name="user" value="isAdmin" />isAdmin</label>
|
package/package.json
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
|
|
3
|
+
export const infoIcon = html`<svg
|
|
4
|
+
height="20"
|
|
5
|
+
width="20"
|
|
6
|
+
viewBox="0 0 100 100"
|
|
7
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
8
|
+
>
|
|
9
|
+
<g fill="none" fill-rule="evenodd">
|
|
10
|
+
<path d="m0 0h100v100h-100z" fill="#fff" />
|
|
11
|
+
<path
|
|
12
|
+
d="m49.8315487 0h.1702245c6.7356878 0 13.1853038 1.31117332 19.3488483 3.93351997 6.1635444 2.62234664 11.4854233 6.15778963 15.9656369 10.60632903 4.4802135 4.4485394 8.0478347 9.7522946 10.7028636 15.9112655 2.655029 6.1589709 3.980878 12.6038012 3.9789971 19.3344909.0567419 6.6716279-1.1702933 13.0585776-3.6811042 19.1608491-2.510811 6.1022715-6.106803 11.5206067-10.7879759 16.2550055-9.7027949 9.7522946-21.4884754 14.6851412-35.3570414 14.79854h-.1702244c-6.7333236 0-13.1829397-1.3111733-19.3488483-3.93352-6.1659087-2.6223466-11.4877876-6.1577896-15.9656369-10.606329s-8.04547055-9.7522946-10.7028637-15.9112655c-2.65739314-6.1589709-3.9844243-12.6038012-3.98254337-19.3344909-.05674149-6.6716279 1.17029325-13.0585776 3.68110421-19.1608491 2.51081095-6.1022715 6.10680292-11.5206067 10.78797586-16.2550055 9.7027949-9.75229456 21.4884754-14.68514123 35.3570414-14.79854zm12.6566146 26.4757998c1.6745578-1.6828001 2.5118367-3.6747334 2.5118367-5.9757998 0-2.4126333-.8095238-4.4324583-2.4285714-6.059475s-3.6289796-2.440525-6.0297959-2.440525c-2.4008164 0-4.4107483.8135083-6.029796 2.440525-1.6745578 1.6270167-2.5118367 3.6468417-2.5118367 6.059475 0 2.1871753.8372789 4.1791086 2.5118367 5.9757998 1.6745579 1.6828001 3.6844898 2.5242002 6.029796 2.5242002 2.3453061 0 4.3274829-.8414001 5.9465306-2.5242002zm-12.1370589 52.7776981-1.2815282-.9486968c0-.4588935.398855-1.8938272 1.196565-4.3048011l12.7338588-39-23.0745876 3.6164609.2548896 3.873251c0-.1141289.4554971-.1997256 1.3664914-.2567901.9109942 0 1.623741.1723823 2.1382404.5171468.5121392.2306356.7965299.6039323.8531721 1.1198902 0 .8607225-.6549247 3.2134431-1.9647739 7.0581619l-8.1175252 24.1061729c-1.0242785 3.2716963-1.5080967 5.5388203-1.4514546 6.8013717.0566421 1.6643804.8826732 2.9839963 2.4780932 3.9588477 1.2532071.803658 2.8769482 1.205487 4.8712231 1.205487h.5982825c1.7653464-.0570645 3.8445846-.3875629 6.2377146-.9914952 2.3931299-.6039323 4.3590839-1.3933242 5.8978617-2.3681756 1.3098493-.803658 2.3919499-1.5359853 3.2463021-2.1969821.8543521-.6609968 1.3959925-1.1341564 1.6249211-1.4194788l.3433929-.3459533-2.8214861-3.3596708-2.9914125 2.0650205c-.79771.4588935-1.5104568.7454047-2.1382404.8595337z"
|
|
13
|
+
fill="#000"
|
|
14
|
+
fill-rule="nonzero"
|
|
15
|
+
/>
|
|
16
|
+
</g>
|
|
17
|
+
</svg> `;
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
/* eslint-disable */
|
|
2
2
|
import { sentryLogs } from '../../config/sentry-events.js';
|
|
3
|
+
import {
|
|
4
|
+
sentryCaptureMessage,
|
|
5
|
+
sentryCaptureException,
|
|
6
|
+
} from '../sentry-report.js';
|
|
3
7
|
import log from '../log.js';
|
|
4
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Patron-facing messages for failures that never reached the lending
|
|
11
|
+
* service's own JSON error handling.
|
|
12
|
+
*/
|
|
13
|
+
export const unexpectedResponseError = status =>
|
|
14
|
+
`Unexpected response from the lending service (HTTP ${status}). Please try again.`;
|
|
15
|
+
export const networkError =
|
|
16
|
+
'Could not reach the lending service. Please check your connection and try again.';
|
|
17
|
+
export const unexpectedError =
|
|
18
|
+
'Something went wrong handling the lending service response. Please try again.';
|
|
19
|
+
|
|
5
20
|
/**
|
|
6
21
|
* Helper to call loan service
|
|
7
22
|
* @param {Object} options
|
|
@@ -18,7 +33,6 @@ export default async function ActionsHandlerService(options) {
|
|
|
18
33
|
let baseHost = '/services/loans/loan';
|
|
19
34
|
const location = window?.location;
|
|
20
35
|
|
|
21
|
-
// return error reponse when not production and has ?error=true param...
|
|
22
36
|
const tokenError = 'loan token not found. please try again later.';
|
|
23
37
|
const borrowError =
|
|
24
38
|
'This book is not available to borrow at this time. Please try again later.';
|
|
@@ -29,9 +43,18 @@ export default async function ActionsHandlerService(options) {
|
|
|
29
43
|
'renew_loan',
|
|
30
44
|
'return_loan',
|
|
31
45
|
];
|
|
46
|
+
const searchParams = new URLSearchParams(location?.search);
|
|
47
|
+
// ?error=true fails every erroneous action (e.g. simulates the book
|
|
48
|
+
// already being borrowed by someone else). ?failAction=create_token (or
|
|
49
|
+
// any action name) fails ONLY that action, so a renew_loan can succeed
|
|
50
|
+
// and the immediately-following create_token can be made to fail —
|
|
51
|
+
// reproducing WEBDEV-8322's "renewed, but create_token failed" bug
|
|
52
|
+
// without needing a real book.
|
|
32
53
|
const shouldReturnError =
|
|
33
|
-
|
|
34
|
-
|
|
54
|
+
location?.hostname !== 'archive.org' &&
|
|
55
|
+
erroneousActions.includes(option?.action) &&
|
|
56
|
+
(searchParams.get('error') === 'true' ||
|
|
57
|
+
searchParams.get('failAction') === option?.action);
|
|
35
58
|
|
|
36
59
|
const testHostname = ['localhost', 'internetarchive.github.io'];
|
|
37
60
|
let isTest = false;
|
|
@@ -44,14 +67,21 @@ export default async function ActionsHandlerService(options) {
|
|
|
44
67
|
formData.append('action', option.action);
|
|
45
68
|
formData.append('identifier', option.identifier);
|
|
46
69
|
|
|
70
|
+
// Set once a response arrives, so the catch below can tell a failed fetch
|
|
71
|
+
// (a real network problem) from anything thrown afterwards — including by
|
|
72
|
+
// the caller's own success/error callback, which run inside this try.
|
|
73
|
+
let reachedResponse = false;
|
|
74
|
+
|
|
47
75
|
try {
|
|
48
76
|
await fetch(baseHost, {
|
|
49
77
|
method: 'POST',
|
|
50
78
|
body: formData,
|
|
51
79
|
})
|
|
52
80
|
.then(async response => {
|
|
81
|
+
reachedResponse = true;
|
|
82
|
+
|
|
53
83
|
// intentional error on localhost
|
|
54
|
-
if (shouldReturnError
|
|
84
|
+
if (shouldReturnError) {
|
|
55
85
|
return {
|
|
56
86
|
success: false,
|
|
57
87
|
error: option?.action === 'create_token' ? tokenError : borrowError,
|
|
@@ -77,9 +107,7 @@ export default async function ActionsHandlerService(options) {
|
|
|
77
107
|
};
|
|
78
108
|
}
|
|
79
109
|
|
|
80
|
-
|
|
81
|
-
// You parse the data into a useable format using `.json()`
|
|
82
|
-
return response.json();
|
|
110
|
+
return parseServiceResponse(response, option);
|
|
83
111
|
})
|
|
84
112
|
.then(data => {
|
|
85
113
|
// `data` is the parsed version of the JSON returned from the above endpoint.
|
|
@@ -92,8 +120,100 @@ export default async function ActionsHandlerService(options) {
|
|
|
92
120
|
}
|
|
93
121
|
});
|
|
94
122
|
} catch (error) {
|
|
95
|
-
|
|
96
|
-
|
|
123
|
+
sentryCaptureException(error, {
|
|
124
|
+
identifier: option.identifier,
|
|
125
|
+
action: option.action,
|
|
126
|
+
extra: {
|
|
127
|
+
message: `${sentryLogs.actionsHandlerService} - ${option.action} ${
|
|
128
|
+
reachedResponse ? 'threw' : 'fetch failed'
|
|
129
|
+
}`,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Report it, don't just swallow it. A rejected fetch (offline, dropped
|
|
135
|
+
* connection) lands here — a non-JSON body no longer does, see
|
|
136
|
+
* parseServiceResponse() — as does anything thrown after the response
|
|
137
|
+
* arrived; calling neither `success` nor `error` leaves every caller
|
|
138
|
+
* waiting on a callback that never comes. Only the former is a network
|
|
139
|
+
* problem, so only that one says so.
|
|
140
|
+
*
|
|
141
|
+
* For renew_loan that's not merely a missing modal: IABookActions
|
|
142
|
+
* clears its loanRenewInProgress guard from these callbacks, so a
|
|
143
|
+
* silent failure latched it on for good and every later renewal
|
|
144
|
+
* attempt became a no-op. Backgrounding a tab on a flaky mobile
|
|
145
|
+
* connection is exactly when this fires.
|
|
146
|
+
*/
|
|
147
|
+
log(`[IABookActions] ✗ ${option.action} threw`, error);
|
|
148
|
+
option?.error?.({
|
|
149
|
+
error: reachedResponse ? unexpectedError : networkError,
|
|
150
|
+
detail: String(error),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Parse a lending service response without ever surfacing a raw
|
|
157
|
+
* `SyntaxError` to the patron. Anything in front of PHP (the WAF, or nginx
|
|
158
|
+
* swapping in /about/400.html or a 5xx page) can answer with HTML instead
|
|
159
|
+
* of the service's JSON; that becomes a readable error carrying the HTTP
|
|
160
|
+
* status, and is reported to Sentry with the identifier/action.
|
|
161
|
+
*
|
|
162
|
+
* @param {Response} response
|
|
163
|
+
* @param {Object} option - the ActionsHandlerService options
|
|
164
|
+
* @returns {Promise<Object>}
|
|
165
|
+
*/
|
|
166
|
+
export async function parseServiceResponse(response, option) {
|
|
167
|
+
const { status } = response;
|
|
168
|
+
const contentType = response.headers?.get('content-type') || '';
|
|
169
|
+
|
|
170
|
+
// The service always answers with JSON (application/json, or
|
|
171
|
+
// application/problem+json for OPDS) — anything else never came from it.
|
|
172
|
+
const body = await response.text();
|
|
173
|
+
let data;
|
|
174
|
+
if (contentType.includes('json')) {
|
|
175
|
+
try {
|
|
176
|
+
data = JSON.parse(body);
|
|
177
|
+
} catch (parseError) {
|
|
178
|
+
data = undefined;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (data === undefined) {
|
|
183
|
+
sentryCaptureMessage(
|
|
184
|
+
`${sentryLogs.actionsHandlerService} - ${option.action}: non-JSON response (HTTP ${status})`,
|
|
185
|
+
{
|
|
186
|
+
identifier: option.identifier,
|
|
187
|
+
action: option.action,
|
|
188
|
+
extra: {
|
|
189
|
+
status,
|
|
190
|
+
contentType,
|
|
191
|
+
bodySnippet: body.slice(0, 300),
|
|
192
|
+
},
|
|
193
|
+
}
|
|
97
194
|
);
|
|
195
|
+
// nonJsonResponse lets callers tell "something in front of PHP answered"
|
|
196
|
+
// apart from the service's own error — see
|
|
197
|
+
// IABookActions.handleLendingActionError.
|
|
198
|
+
return {
|
|
199
|
+
success: false,
|
|
200
|
+
error: unexpectedResponseError(status),
|
|
201
|
+
status,
|
|
202
|
+
nonJsonResponse: true,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Carry the HTTP status on the service's own JSON errors too, so it
|
|
207
|
+
// reaches Sentry. A JSON error status without the service's usual `error`
|
|
208
|
+
// field would otherwise be treated as a success.
|
|
209
|
+
if (!response.ok) {
|
|
210
|
+
return {
|
|
211
|
+
...(data && typeof data === 'object' ? data : {}),
|
|
212
|
+
success: false,
|
|
213
|
+
error: data?.error || unexpectedResponseError(status),
|
|
214
|
+
status,
|
|
215
|
+
};
|
|
98
216
|
}
|
|
217
|
+
|
|
218
|
+
return data;
|
|
99
219
|
}
|
|
@@ -2,11 +2,19 @@ import { LitElement } from 'lit';
|
|
|
2
2
|
|
|
3
3
|
import { URLHelper } from '../../config/url-helper.js';
|
|
4
4
|
import { sentryLogs } from '../../config/sentry-events.js';
|
|
5
|
+
import {
|
|
6
|
+
sentryCaptureMessage,
|
|
7
|
+
sentryCaptureException,
|
|
8
|
+
} from '../sentry-report.js';
|
|
5
9
|
import log from '../log.js';
|
|
6
10
|
|
|
7
11
|
import ActionsHandlerService from './actions-handler-service.js';
|
|
8
12
|
import LoanAnanlytics from '../loan-analytics.js';
|
|
9
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
analyticsCategories,
|
|
15
|
+
analyticsActions,
|
|
16
|
+
analyticsLabels,
|
|
17
|
+
} from '../../config/analytics-event-and-category.js';
|
|
10
18
|
import * as Cookies from '../doc-cookies.js';
|
|
11
19
|
|
|
12
20
|
/**
|
|
@@ -172,32 +180,51 @@ export default class ActionsHandler extends LitElement {
|
|
|
172
180
|
action,
|
|
173
181
|
identifier: this.identifier,
|
|
174
182
|
success: async data => {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
183
|
+
// Anything thrown in here is an unhandled rejection that dispatches
|
|
184
|
+
// NOTHING — and IABookActions only clears loanRenewInProgress when
|
|
185
|
+
// one of these two events arrives, so a throw here latches it on
|
|
186
|
+
// permanently (dead countdown, loan stuck "active"). Always exit
|
|
187
|
+
// through exactly one of the two dispatches below.
|
|
188
|
+
try {
|
|
189
|
+
log('RENEW_LOAN --- ', data, this.identifier);
|
|
190
|
+
const activeLoan = data.loan ? data.loan : undefined;
|
|
191
|
+
// Optional chaining: `data.loan` is genuinely absent on some
|
|
192
|
+
// failures, and `activeLoan` can be undefined here.
|
|
193
|
+
const isRenewal = activeLoan?.renewal;
|
|
194
|
+
|
|
195
|
+
if (activeLoan && isRenewal) {
|
|
196
|
+
// Await — loanAutoRenewed listeners read this same cache key
|
|
197
|
+
// immediately; dispatching before the write lands was a race.
|
|
198
|
+
await this.setBrowseTimeSession();
|
|
199
|
+
|
|
200
|
+
// Only record the renew analytics event once the renewal has
|
|
201
|
+
// actually succeeded — not merely attempted.
|
|
202
|
+
await this.loanAnanlytics?.storeLoanStatsCount(
|
|
203
|
+
this.identifier,
|
|
204
|
+
'autorenew'
|
|
205
|
+
);
|
|
206
|
+
const analyticsLabel =
|
|
207
|
+
renewType === 'auto'
|
|
208
|
+
? analyticsLabels.browseAutoRenew
|
|
209
|
+
: analyticsLabels.browseManualRenew;
|
|
210
|
+
this.loanAnanlytics?.sendEvent(
|
|
211
|
+
analyticsCategories.browse,
|
|
212
|
+
analyticsActions.browseRenew,
|
|
213
|
+
analyticsLabel,
|
|
214
|
+
this.identifier
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// Dispatch the success outcome ONLY for a confirmed renewal —
|
|
218
|
+
// a `{loan: {renewal: false}}` response goes through
|
|
219
|
+
// dispatchActionError below instead, never both.
|
|
220
|
+
this.dispatchEvent(
|
|
221
|
+
new CustomEvent('loanAutoRenewed', {
|
|
222
|
+
detail: { action, data: { ...data, loan: activeLoan } },
|
|
223
|
+
})
|
|
224
|
+
);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
201
228
|
log('RENEW_LOAN ERROR --- ', {
|
|
202
229
|
action,
|
|
203
230
|
isRenewal,
|
|
@@ -205,22 +232,32 @@ export default class ActionsHandler extends LitElement {
|
|
|
205
232
|
data,
|
|
206
233
|
id: this.identifier,
|
|
207
234
|
});
|
|
208
|
-
|
|
209
|
-
`${sentryLogs.bookRenewFailed} -
|
|
235
|
+
sentryCaptureMessage(
|
|
236
|
+
`${sentryLogs.bookRenewFailed} - no loan active`,
|
|
237
|
+
{
|
|
238
|
+
identifier: this.identifier,
|
|
239
|
+
action,
|
|
240
|
+
extra: { response: JSON.stringify(data) },
|
|
241
|
+
}
|
|
210
242
|
);
|
|
211
243
|
this.dispatchActionError(action, {
|
|
212
244
|
data,
|
|
213
245
|
error: true,
|
|
214
246
|
message: 'Loan renewal failed: no loan active.',
|
|
215
247
|
});
|
|
248
|
+
} catch (error) {
|
|
249
|
+
log('RENEW_LOAN THREW --- ', error);
|
|
250
|
+
sentryCaptureException(error, {
|
|
251
|
+
identifier: this.identifier,
|
|
252
|
+
action,
|
|
253
|
+
extra: { message: `${sentryLogs.bookRenewFailed} - threw` },
|
|
254
|
+
});
|
|
255
|
+
this.dispatchActionError(action, {
|
|
256
|
+
data,
|
|
257
|
+
error: true,
|
|
258
|
+
message: `Loan renewal failed: ${error}`,
|
|
259
|
+
});
|
|
216
260
|
}
|
|
217
|
-
|
|
218
|
-
// dispatch outcome of loan renewal
|
|
219
|
-
this.dispatchEvent(
|
|
220
|
-
new CustomEvent('loanAutoRenewed', {
|
|
221
|
-
detail: { action, data: { ...data, loan: activeLoan } },
|
|
222
|
-
})
|
|
223
|
-
);
|
|
224
261
|
},
|
|
225
262
|
error: data => {
|
|
226
263
|
this.dispatchActionError(action, data);
|
|
@@ -372,6 +409,12 @@ export default class ActionsHandler extends LitElement {
|
|
|
372
409
|
new Date().getTime() + this.loanTotalTime * 1000
|
|
373
410
|
);
|
|
374
411
|
|
|
412
|
+
log('[ActionsHandler] setBrowseTimeSession: resetting loanTime', {
|
|
413
|
+
identifier: this.identifier,
|
|
414
|
+
expireDate,
|
|
415
|
+
loanTotalTime: this.loanTotalTime,
|
|
416
|
+
});
|
|
417
|
+
|
|
375
418
|
// set a value
|
|
376
419
|
await this.localCache.set({
|
|
377
420
|
key: `${this.identifier}-loanTime`,
|
|
@@ -381,12 +424,20 @@ export default class ActionsHandler extends LitElement {
|
|
|
381
424
|
|
|
382
425
|
// delete pageChangedTime when book is auto renew at nth minute
|
|
383
426
|
await this.localCache.delete(`${this.identifier}-pageChangedTime`);
|
|
427
|
+
|
|
428
|
+
log('[ActionsHandler] setBrowseTimeSession: loanTime reset complete', {
|
|
429
|
+
identifier: this.identifier,
|
|
430
|
+
});
|
|
384
431
|
} catch (error) {
|
|
385
|
-
log(error);
|
|
432
|
+
log('[ActionsHandler] setBrowseTimeSession failed', error);
|
|
386
433
|
}
|
|
387
434
|
}
|
|
388
435
|
|
|
389
436
|
deleteLoanCookies() {
|
|
437
|
+
log('[ActionsHandler] deleteLoanCookies: expiring loan cookies', {
|
|
438
|
+
identifier: this.identifier,
|
|
439
|
+
});
|
|
440
|
+
|
|
390
441
|
const date = new Date();
|
|
391
442
|
date.setTime(date.getTime() - 24 * 60 * 60 * 1000); // one day ago
|
|
392
443
|
|
|
@@ -414,7 +465,8 @@ export default class ActionsHandler extends LitElement {
|
|
|
414
465
|
* @returns {void}
|
|
415
466
|
*/
|
|
416
467
|
setStickyAdminAccess(value) {
|
|
417
|
-
const domain =
|
|
468
|
+
const domain =
|
|
469
|
+
window.location.hostname === 'localhost' ? 'localhost' : '.archive.org';
|
|
418
470
|
const expires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days from now
|
|
419
471
|
Cookies.setItem('sticky-admin-access', value, expires, '/', domain);
|
|
420
472
|
}
|
|
@@ -159,17 +159,6 @@ export default class LoanAnanlytics {
|
|
|
159
159
|
* @memberof LoanAnanlytics
|
|
160
160
|
*/
|
|
161
161
|
sendEvent(eventCategory, eventAction, label, extraParams) {
|
|
162
|
-
log(
|
|
163
|
-
'eventCategory:-',
|
|
164
|
-
eventCategory,
|
|
165
|
-
'||\teventAction:-',
|
|
166
|
-
eventAction,
|
|
167
|
-
'||\tlabel:-',
|
|
168
|
-
label,
|
|
169
|
-
'||\textraParams:-',
|
|
170
|
-
extraParams,
|
|
171
|
-
);
|
|
172
|
-
|
|
173
162
|
window?.archive_analytics?.send_event_no_sampling(
|
|
174
163
|
eventCategory,
|
|
175
164
|
eventAction,
|
|
@@ -26,7 +26,7 @@ export class LoanRenewHelper {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
handleLoanRenew() {
|
|
30
30
|
try {
|
|
31
31
|
if (this.hasPageChanged) {
|
|
32
32
|
return this.pageChanged(); // user clicked on page
|
|
@@ -99,7 +99,7 @@ export class LoanRenewHelper {
|
|
|
99
99
|
} else if (pageChangedTime >= pageChangeTimeFrame) {
|
|
100
100
|
this.result = {
|
|
101
101
|
texts: '',
|
|
102
|
-
renewNow: true,
|
|
102
|
+
renewNow: true, // viewed in last time frame
|
|
103
103
|
renewType: 'auto',
|
|
104
104
|
};
|
|
105
105
|
}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import ActionsHandlerService from './actions-handler/actions-handler-service.js';
|
|
3
3
|
import LoanAnanlytics from './loan-analytics.js';
|
|
4
4
|
import { sentryLogs } from '../config/sentry-events.js';
|
|
5
|
+
import { sentryCaptureMessage } from './sentry-report.js';
|
|
6
|
+
import log from './log.js';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* This class is used to create loan token for borrowed books
|
|
@@ -9,12 +11,39 @@ import { sentryLogs } from '../config/sentry-events.js';
|
|
|
9
11
|
* ActionsHandlerService is a function being used to execute
|
|
10
12
|
*/
|
|
11
13
|
export class LoanTokenPoller {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} options
|
|
16
|
+
* @param {string} options.identifier
|
|
17
|
+
* @param {string} options.borrowType
|
|
18
|
+
* @param {Function} options.successCallback - called after the initial
|
|
19
|
+
* loan token is created.
|
|
20
|
+
* @param {Function} options.errorCallback - called on any create_token
|
|
21
|
+
* failure (initial or a routine interval tick).
|
|
22
|
+
* @param {number} options.pollerDelay - interval between routine
|
|
23
|
+
* create_token checks, in seconds.
|
|
24
|
+
* @param {boolean} [options.skipInitialCall] - skip the immediate
|
|
25
|
+
* create_token call, only start the recurring interval. Used after a
|
|
26
|
+
* loan-expiry recovery renewal, where BookLoanService::
|
|
27
|
+
* attempt_to_renew_loan() already minted a valid access token as part
|
|
28
|
+
* of the renew_loan response itself — an immediate confirming
|
|
29
|
+
* create_token call is redundant.
|
|
30
|
+
*/
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
const {
|
|
33
|
+
identifier,
|
|
34
|
+
borrowType,
|
|
35
|
+
successCallback,
|
|
36
|
+
errorCallback,
|
|
37
|
+
pollerDelay,
|
|
38
|
+
skipInitialCall = false,
|
|
39
|
+
} = options;
|
|
40
|
+
|
|
41
|
+
this.identifier = identifier;
|
|
14
42
|
this.borrowType = borrowType;
|
|
15
43
|
this.successCallback = successCallback; // callback function to be called after loan token is created
|
|
16
44
|
this.errorCallback = errorCallback; // callback function to be called after loan token is created
|
|
17
45
|
this.pollerDelay = pollerDelay; // value in seconds
|
|
46
|
+
this.skipInitialCall = skipInitialCall === true;
|
|
18
47
|
|
|
19
48
|
this.loanTokenInterval = undefined;
|
|
20
49
|
|
|
@@ -33,8 +62,17 @@ export class LoanTokenPoller {
|
|
|
33
62
|
|
|
34
63
|
async bookAccessed() {
|
|
35
64
|
if (this.borrowType) {
|
|
36
|
-
|
|
37
|
-
|
|
65
|
+
if (this.skipInitialCall) {
|
|
66
|
+
log(
|
|
67
|
+
'[LoanTokenPoller] skipping initial create_token — already minted by the renewal response',
|
|
68
|
+
{
|
|
69
|
+
identifier: this.identifier,
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
} else {
|
|
73
|
+
// Do an initial token, then set an interval
|
|
74
|
+
this.handleLoanTokenPoller(true);
|
|
75
|
+
}
|
|
38
76
|
|
|
39
77
|
// if this.borrowType = adminBorrowed,
|
|
40
78
|
// - we don't want to fetch token on interval
|
|
@@ -49,38 +87,86 @@ export class LoanTokenPoller {
|
|
|
49
87
|
}, this.pollerDelay * 1000);
|
|
50
88
|
}
|
|
51
89
|
} else {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
90
|
+
sentryCaptureMessage(`${sentryLogs.bookAccessed} - not borrowed`, {
|
|
91
|
+
identifier: this.identifier,
|
|
92
|
+
action: 'create_token',
|
|
93
|
+
});
|
|
55
94
|
|
|
56
95
|
// if book is not browsed, just clear token polling interval
|
|
57
96
|
this.disconnectedCallback();
|
|
58
97
|
}
|
|
59
98
|
}
|
|
60
99
|
|
|
100
|
+
/**
|
|
101
|
+
* @param {boolean} isInitial - the first create_token call right after
|
|
102
|
+
* bookAccessed()/a renewal, as opposed to a routine interval tick.
|
|
103
|
+
*/
|
|
61
104
|
async handleLoanTokenPoller(isInitial = false) {
|
|
62
105
|
const action = 'create_token';
|
|
106
|
+
log('[LoanTokenPoller] create_token requested', {
|
|
107
|
+
identifier: this.identifier,
|
|
108
|
+
isInitial,
|
|
109
|
+
});
|
|
63
110
|
ActionsHandlerService({
|
|
64
111
|
identifier: this.identifier,
|
|
65
112
|
action,
|
|
66
|
-
error: data =>
|
|
67
|
-
this.errorCallback({ detail: { action, data } });
|
|
68
|
-
|
|
69
|
-
// send error to Sentry
|
|
70
|
-
window?.Sentry?.captureMessage(
|
|
71
|
-
`${sentryLogs.handleLoanTokenPoller} - Error: ${JSON.stringify(data)}`
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
// send LendingServiceError to GA
|
|
75
|
-
this.loanAnalytics?.sendEvent(
|
|
76
|
-
'LendingServiceLoanError',
|
|
77
|
-
action,
|
|
78
|
-
this.identifier
|
|
79
|
-
);
|
|
80
|
-
},
|
|
113
|
+
error: data => this.handleTokenError(data, isInitial),
|
|
81
114
|
success: () => {
|
|
115
|
+
log('[LoanTokenPoller] create_token succeeded', {
|
|
116
|
+
identifier: this.identifier,
|
|
117
|
+
isInitial,
|
|
118
|
+
});
|
|
82
119
|
if (isInitial) this.successCallback();
|
|
83
120
|
},
|
|
84
121
|
});
|
|
85
122
|
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Reports every create_token failure via errorCallback — the poller never
|
|
126
|
+
* retries, and the consumer decides whether a failure is terminal (see
|
|
127
|
+
* IABookActions.handleLendingActionError). Split out from
|
|
128
|
+
* handleLoanTokenPoller so it's directly testable without needing to
|
|
129
|
+
* mock the network call.
|
|
130
|
+
*
|
|
131
|
+
* @param {Object} data - the error payload from ActionsHandlerService.
|
|
132
|
+
* @param {boolean} isInitial
|
|
133
|
+
*/
|
|
134
|
+
handleTokenError(data, isInitial) {
|
|
135
|
+
const action = 'create_token';
|
|
136
|
+
|
|
137
|
+
log('[LoanTokenPoller] create_token failed', {
|
|
138
|
+
identifier: this.identifier,
|
|
139
|
+
isInitial,
|
|
140
|
+
error: data?.error,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// isInitial rides along so the consumer can distinguish "the book won't
|
|
144
|
+
// open at all" from "a mid-read refresh blipped" — see
|
|
145
|
+
// IABookActions.handleLendingActionError.
|
|
146
|
+
this.errorCallback({ detail: { action, data, isInitial } });
|
|
147
|
+
|
|
148
|
+
// send error to Sentry
|
|
149
|
+
sentryCaptureMessage(
|
|
150
|
+
`${sentryLogs.handleLoanTokenPoller} - create_token failed`,
|
|
151
|
+
{
|
|
152
|
+
identifier: this.identifier,
|
|
153
|
+
action,
|
|
154
|
+
extra: {
|
|
155
|
+
borrowType: this.borrowType,
|
|
156
|
+
isInitial,
|
|
157
|
+
error: data?.error,
|
|
158
|
+
status: data?.status,
|
|
159
|
+
nonJsonResponse: data?.nonJsonResponse === true,
|
|
160
|
+
response: JSON.stringify(data),
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// send LendingServiceError to GA
|
|
166
|
+
this.loanAnalytics?.sendEvent(
|
|
167
|
+
'LendingServiceLoanError',
|
|
168
|
+
action,
|
|
169
|
+
this.identifier
|
|
170
|
+
);
|
|
171
|
+
}
|
|
86
172
|
}
|