clarity-js 0.7.3 → 0.7.5
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/build/clarity.insight.js +1 -1
- package/build/clarity.js +1312 -1325
- package/build/clarity.min.js +1 -1
- package/build/clarity.module.js +1312 -1325
- package/package.json +1 -1
- package/src/core/config.ts +0 -1
- package/src/core/report.ts +1 -1
- package/src/core/version.ts +1 -1
- package/src/data/encode.ts +1 -1
- package/src/data/extract.ts +45 -34
- package/src/data/upload.ts +26 -15
- package/src/layout/dom.ts +4 -35
- package/src/layout/mutation.ts +4 -4
- package/src/layout/node.ts +2 -2
- package/src/performance/observer.ts +1 -1
- package/types/core.d.ts +0 -2
- package/types/data.d.ts +8 -4
- package/types/layout.d.ts +0 -1
package/package.json
CHANGED
package/src/core/config.ts
CHANGED
package/src/core/report.ts
CHANGED
|
@@ -19,7 +19,7 @@ export function report(e: Error): Error {
|
|
|
19
19
|
// Using POST request instead of a GET request (img-src) to not violate existing CSP rules
|
|
20
20
|
// Since, Clarity already uses XHR to upload data, we stick with similar POST mechanism for reporting too
|
|
21
21
|
let xhr = new XMLHttpRequest();
|
|
22
|
-
xhr.open("POST", url);
|
|
22
|
+
xhr.open("POST", url, true);
|
|
23
23
|
xhr.send(JSON.stringify(payload));
|
|
24
24
|
history.push(e.message);
|
|
25
25
|
}
|
package/src/core/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
let version = "0.7.
|
|
1
|
+
let version = "0.7.5";
|
|
2
2
|
export default version;
|
package/src/data/encode.ts
CHANGED
|
@@ -110,7 +110,7 @@ export default function(event: Event): void {
|
|
|
110
110
|
let extractKeys = extract.keys;
|
|
111
111
|
for (let e of extractKeys) {
|
|
112
112
|
tokens.push(e);
|
|
113
|
-
tokens.push(extract.data[e]);
|
|
113
|
+
tokens.push([].concat(...extract.data[e]));
|
|
114
114
|
}
|
|
115
115
|
extract.reset();
|
|
116
116
|
queue(tokens, false);
|
package/src/data/extract.ts
CHANGED
|
@@ -1,38 +1,36 @@
|
|
|
1
1
|
import { ExtractSource, Syntax, Type } from "@clarity-types/core";
|
|
2
2
|
import { Event, Setting, ExtractData } from "@clarity-types/data";
|
|
3
|
-
import config from "@src/core/config";
|
|
4
3
|
import encode from "./encode";
|
|
5
4
|
import * as internal from "@src/diagnostic/internal";
|
|
6
5
|
import { Code, Constant, Severity } from "@clarity-types/data";
|
|
7
6
|
|
|
8
7
|
export let data: ExtractData = {};
|
|
9
|
-
export let keys:
|
|
10
|
-
|
|
11
|
-
let variables : { [key: number]: Syntax[] } = {};
|
|
12
|
-
let selectors : { [key: number]: string } = {};
|
|
13
|
-
export let fragments: string[] = [];
|
|
8
|
+
export let keys: number[] = [];
|
|
14
9
|
|
|
10
|
+
let variables : { [key: number]: { [key: number]: Syntax[] }} = {};
|
|
11
|
+
let selectors : { [key: number]: { [key: number]: string }} = {};
|
|
15
12
|
export function start(): void {
|
|
13
|
+
reset();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function trigger(input: string): void {
|
|
16
17
|
try {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
var parts = input && input.length > 0 ? input.split(/ (.*)/) : [Constant.Empty];
|
|
19
|
+
var key = parseInt(parts[0]);
|
|
20
|
+
var values = parts.length > 1 ? JSON.parse(parts[1]) : {};
|
|
21
|
+
variables[key] = {};
|
|
22
|
+
selectors[key] = {};
|
|
23
|
+
for (var v in values) {
|
|
24
|
+
let id = parseInt(v);
|
|
25
|
+
let value = values[v] as string;
|
|
26
|
+
let source = value.startsWith(Constant.Tilde) ? ExtractSource.Javascript : ExtractSource.Text;
|
|
22
27
|
switch (source) {
|
|
23
28
|
case ExtractSource.Javascript:
|
|
24
|
-
let variable =
|
|
25
|
-
variables[key] = parse(variable);
|
|
26
|
-
break;
|
|
27
|
-
case ExtractSource.Cookie:
|
|
28
|
-
/*Todo: Add cookie extract logic*/
|
|
29
|
+
let variable = value.substring(1, value.length);
|
|
30
|
+
variables[key][id] = parse(variable);
|
|
29
31
|
break;
|
|
30
32
|
case ExtractSource.Text:
|
|
31
|
-
|
|
32
|
-
selectors[key] = match;
|
|
33
|
-
break;
|
|
34
|
-
case ExtractSource.Fragment:
|
|
35
|
-
fragments = e[i+2] as string[];
|
|
33
|
+
selectors[key][id] = value;
|
|
36
34
|
break;
|
|
37
35
|
}
|
|
38
36
|
}
|
|
@@ -49,13 +47,25 @@ export function clone(v: Syntax[]): Syntax[] {
|
|
|
49
47
|
export function compute(): void {
|
|
50
48
|
try {
|
|
51
49
|
for (let v in variables) {
|
|
52
|
-
let
|
|
53
|
-
if (
|
|
54
|
-
|
|
50
|
+
let key = parseInt(v);
|
|
51
|
+
if (!(key in keys)) {
|
|
52
|
+
let variableData = variables[key];
|
|
53
|
+
for (let v in variableData) {
|
|
54
|
+
let variableKey = parseInt(v);
|
|
55
|
+
let value = str(evaluate(clone(variableData[variableKey])));
|
|
56
|
+
if (value) { update(key, variableKey, value); }
|
|
57
|
+
}
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
let selectorData = selectors[key];
|
|
60
|
+
for (let s in selectorData) {
|
|
61
|
+
let selectorKey = parseInt(s);
|
|
62
|
+
let nodes = document.querySelectorAll(selectorData[selectorKey]) as NodeListOf<HTMLElement>;
|
|
63
|
+
if (nodes) {
|
|
64
|
+
let text = Array.from(nodes).map(e => e.innerText)
|
|
65
|
+
update(key, selectorKey, text.join(Constant.Seperator).substring(0, Setting.ExtractLimit));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
59
69
|
}
|
|
60
70
|
}
|
|
61
71
|
catch (e) { internal.log(Code.Selector, Severity.Warning, e ? e.name : null); }
|
|
@@ -64,21 +74,22 @@ export function compute(): void {
|
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
export function reset(): void {
|
|
77
|
+
data = {};
|
|
67
78
|
keys = [];
|
|
79
|
+
variables = {};
|
|
80
|
+
selectors = {};
|
|
68
81
|
}
|
|
69
82
|
|
|
70
|
-
export function update(key:
|
|
71
|
-
if (!(key in data)
|
|
72
|
-
data[key] =
|
|
83
|
+
export function update(key: number, subkey: number, value: string): void {
|
|
84
|
+
if (!(key in data)) {
|
|
85
|
+
data[key] = []
|
|
73
86
|
keys.push(key);
|
|
74
87
|
}
|
|
88
|
+
data[key].push([subkey, value]);
|
|
75
89
|
}
|
|
76
90
|
|
|
77
91
|
export function stop(): void {
|
|
78
|
-
|
|
79
|
-
keys = [];
|
|
80
|
-
variables = {};
|
|
81
|
-
selectors = {};
|
|
92
|
+
reset();
|
|
82
93
|
}
|
|
83
94
|
|
|
84
95
|
function parse(variable: string): Syntax[] {
|
package/src/data/upload.ts
CHANGED
|
@@ -15,6 +15,8 @@ import * as metric from "@src/data/metric";
|
|
|
15
15
|
import * as ping from "@src/data/ping";
|
|
16
16
|
import * as timeline from "@src/interaction/timeline";
|
|
17
17
|
import * as region from "@src/layout/region";
|
|
18
|
+
import * as extract from "@src/data/extract";
|
|
19
|
+
import { report } from "@src/core/report";
|
|
18
20
|
|
|
19
21
|
let discoverBytes: number = 0;
|
|
20
22
|
let playbackBytes: number = 0;
|
|
@@ -168,7 +170,9 @@ function send(payload: string, zipped: Uint8Array, sequence: number, beacon: boo
|
|
|
168
170
|
// Not all browsers support compression API and the support for it in supported browsers is still experimental
|
|
169
171
|
if (sequence in transit) { transit[sequence].attempts++; } else { transit[sequence] = { data: payload, attempts: 1 }; }
|
|
170
172
|
let xhr = new XMLHttpRequest();
|
|
171
|
-
xhr.open("POST", url);
|
|
173
|
+
xhr.open("POST", url, true);
|
|
174
|
+
xhr.timeout = Setting.UploadTimeout;
|
|
175
|
+
xhr.ontimeout = () => { report(new Error(`${Constant.Timeout} : ${url}`)) };
|
|
172
176
|
if (sequence !== null) { xhr.onreadystatechange = (): void => { measure(check)(xhr, sequence); }; }
|
|
173
177
|
xhr.withCredentials = true;
|
|
174
178
|
if (zipped) {
|
|
@@ -242,19 +246,26 @@ function delay(): number {
|
|
|
242
246
|
}
|
|
243
247
|
|
|
244
248
|
function response(payload: string): void {
|
|
245
|
-
let
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
249
|
+
let lines = payload && payload.length > 0 ? payload.split("\n") : [];
|
|
250
|
+
for (var line of lines)
|
|
251
|
+
{
|
|
252
|
+
let parts = line && line.length > 0 ? line.split(/ (.*)/) : [Constant.Empty];
|
|
253
|
+
switch (parts[0]) {
|
|
254
|
+
case Constant.End:
|
|
255
|
+
// Clear out session storage and end the session so we can start fresh the next time
|
|
256
|
+
limit.trigger(Check.Server);
|
|
257
|
+
break;
|
|
258
|
+
case Constant.Upgrade:
|
|
259
|
+
// Upgrade current session to send back playback information
|
|
260
|
+
clarity.upgrade(Constant.Auto);
|
|
261
|
+
break;
|
|
262
|
+
case Constant.Action:
|
|
263
|
+
// Invoke action callback, if configured and has a valid value
|
|
264
|
+
if (config.action && parts.length > 1) { config.action(parts[1]); }
|
|
265
|
+
break;
|
|
266
|
+
case Constant.Extract:
|
|
267
|
+
if (parts.length > 1) { extract.trigger(parts[1]); }
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
259
270
|
}
|
|
260
271
|
}
|
package/src/layout/dom.ts
CHANGED
|
@@ -6,8 +6,6 @@ import hash from "@src/core/hash";
|
|
|
6
6
|
import * as internal from "@src/diagnostic/internal";
|
|
7
7
|
import * as region from "@src/layout/region";
|
|
8
8
|
import * as selector from "@src/layout/selector";
|
|
9
|
-
import * as mutation from "@src/layout/mutation";
|
|
10
|
-
import * as extract from "@src/data/extract";
|
|
11
9
|
let index: number = 1;
|
|
12
10
|
let nodes: Node[] = [];
|
|
13
11
|
let values: NodeValue[] = [];
|
|
@@ -15,7 +13,6 @@ let updateMap: number[] = [];
|
|
|
15
13
|
let hashMap: { [hash: string]: number } = {};
|
|
16
14
|
let override = [];
|
|
17
15
|
let unmask = [];
|
|
18
|
-
let updatedFragments: { [fragment: number]: string } = {};
|
|
19
16
|
let maskText = [];
|
|
20
17
|
let maskExclude = [];
|
|
21
18
|
let maskDisable = [];
|
|
@@ -92,14 +89,12 @@ export function add(node: Node, parent: Node, data: NodeInfo, source: Source): v
|
|
|
92
89
|
let previousId = getPreviousId(node);
|
|
93
90
|
let parentValue: NodeValue = null;
|
|
94
91
|
let regionId = region.exists(node) ? id : null;
|
|
95
|
-
let fragmentId = null;
|
|
96
92
|
let fraudId = fraudMap.has(node) ? fraudMap.get(node) : null;
|
|
97
93
|
let privacyId = config.content ? Privacy.Sensitive : Privacy.TextImage
|
|
98
94
|
if (parentId >= 0 && values[parentId]) {
|
|
99
95
|
parentValue = values[parentId];
|
|
100
96
|
parentValue.children.push(id);
|
|
101
97
|
regionId = regionId === null ? parentValue.region : regionId;
|
|
102
|
-
fragmentId = parentValue.fragment;
|
|
103
98
|
fraudId = fraudId === null ? parentValue.metadata.fraud : fraudId;
|
|
104
99
|
privacyId = parentValue.metadata.privacy;
|
|
105
100
|
}
|
|
@@ -121,13 +116,12 @@ export function add(node: Node, parent: Node, data: NodeInfo, source: Source): v
|
|
|
121
116
|
hash: null,
|
|
122
117
|
region: regionId,
|
|
123
118
|
metadata: { active: true, suspend: false, privacy: privacyId, position: null, fraud: fraudId, size: null },
|
|
124
|
-
fragment: fragmentId,
|
|
125
119
|
};
|
|
126
120
|
|
|
127
121
|
privacy(node, values[id], parentValue);
|
|
128
122
|
updateSelector(values[id]);
|
|
129
123
|
size(values[id]);
|
|
130
|
-
track(id, source
|
|
124
|
+
track(id, source);
|
|
131
125
|
}
|
|
132
126
|
|
|
133
127
|
export function update(node: Node, parent: Node, data: NodeInfo, source: Source): void {
|
|
@@ -181,14 +175,9 @@ export function update(node: Node, parent: Node, data: NodeInfo, source: Source)
|
|
|
181
175
|
}
|
|
182
176
|
}
|
|
183
177
|
|
|
184
|
-
// track node if it is a part of scheduled fragment mutation
|
|
185
|
-
if(value.fragment && updatedFragments[value.fragment]) {
|
|
186
|
-
changed = true;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
178
|
// Update selector
|
|
190
179
|
updateSelector(value);
|
|
191
|
-
track(id, source,
|
|
180
|
+
track(id, source, changed, parentChanged);
|
|
192
181
|
}
|
|
193
182
|
}
|
|
194
183
|
|
|
@@ -299,10 +288,6 @@ function updateSelector(value: NodeValue): void {
|
|
|
299
288
|
value.selector = [selector.get(s, Selector.Alpha), selector.get(s, Selector.Beta)];
|
|
300
289
|
value.hash = value.selector.map(x => x ? hash(x) : null) as [string, string];
|
|
301
290
|
value.hash.forEach(h => hashMap[h] = value.id);
|
|
302
|
-
// Match fragment configuration against both alpha and beta hash
|
|
303
|
-
if (value.hash.some(h => extract.fragments.indexOf(h) !== -1)) {
|
|
304
|
-
value.fragment = value.id;
|
|
305
|
-
}
|
|
306
291
|
}
|
|
307
292
|
|
|
308
293
|
export function hashText(hash: string): string {
|
|
@@ -344,11 +329,7 @@ export function updates(): NodeValue[] {
|
|
|
344
329
|
if (id in values) { output.push(values[id]); }
|
|
345
330
|
}
|
|
346
331
|
updateMap = [];
|
|
347
|
-
|
|
348
|
-
extract.update(updatedFragments[id], id, true)
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
updatedFragments = {}
|
|
332
|
+
|
|
352
333
|
return output;
|
|
353
334
|
}
|
|
354
335
|
|
|
@@ -377,19 +358,7 @@ function getPreviousId(node: Node): number {
|
|
|
377
358
|
return id;
|
|
378
359
|
}
|
|
379
360
|
|
|
380
|
-
function track(id: number, source: Source,
|
|
381
|
-
// if updated node is a part of fragment and the fragment is not being tracked currently, schedule a mutation on the fragment node
|
|
382
|
-
if (fragment && !updatedFragments[fragment]) {
|
|
383
|
-
let node = getNode(fragment)
|
|
384
|
-
let value = getValue(fragment);
|
|
385
|
-
if (node && value) {
|
|
386
|
-
mutation.schedule(node, true);
|
|
387
|
-
value.hash.forEach(h => {
|
|
388
|
-
if(extract.fragments.indexOf(h) !== -1) { updatedFragments[fragment] = h;}
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
361
|
+
function track(id: number, source: Source, changed: boolean = true, parentChanged: boolean = false): void {
|
|
393
362
|
// Keep track of the order in which mutations happened, they may not be sequential
|
|
394
363
|
// Edge case: If an element is added later on, and pre-discovered element is moved as a child.
|
|
395
364
|
// In that case, we need to reorder the pre-discovered element in the update list to keep visualization consistent.
|
package/src/layout/mutation.ts
CHANGED
|
@@ -210,7 +210,7 @@ async function processNodeList(list: NodeList, source: Source, timer: Timer): Pr
|
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
export function schedule(node: Node
|
|
213
|
+
export function schedule(node: Node): Node {
|
|
214
214
|
// Only schedule manual trigger for this node if it's not already in the queue
|
|
215
215
|
if (queue.indexOf(node) < 0) { queue.push(node); }
|
|
216
216
|
|
|
@@ -218,19 +218,19 @@ export function schedule(node: Node, fragment: boolean = false): Node {
|
|
|
218
218
|
// It's common for a webpage to call multiple synchronous "insertRule" / "deleteRule" calls.
|
|
219
219
|
// And in those cases we do not wish to monitor changes multiple times for the same node.
|
|
220
220
|
if (timeout) { clearTimeout(timeout); }
|
|
221
|
-
timeout = setTimeout(() => { trigger(
|
|
221
|
+
timeout = setTimeout(() => { trigger() }, Setting.LookAhead);
|
|
222
222
|
|
|
223
223
|
return node;
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
function trigger(
|
|
226
|
+
function trigger(): void {
|
|
227
227
|
for (let node of queue) {
|
|
228
228
|
// Generate a mutation for this node only if it still exists
|
|
229
229
|
if (node) {
|
|
230
230
|
let shadowRoot = node.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
|
|
231
231
|
// Skip re-processing shadowRoot if it was already discovered
|
|
232
232
|
if (shadowRoot && dom.has(node)) { continue; }
|
|
233
|
-
generate(node, shadowRoot
|
|
233
|
+
generate(node, shadowRoot ? Constant.ChildList : Constant.CharacterData);
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
236
|
queue = [];
|
package/src/layout/node.ts
CHANGED
|
@@ -128,7 +128,7 @@ export default function (node: Node, source: Source): Node {
|
|
|
128
128
|
case "HEAD":
|
|
129
129
|
let head = { tag, attributes };
|
|
130
130
|
let l = insideFrame && node.ownerDocument?.location ? node.ownerDocument.location : location;
|
|
131
|
-
head.attributes[Constant.Base] = l.protocol + "//" + l.
|
|
131
|
+
head.attributes[Constant.Base] = l.protocol + "//" + l.host + l.pathname;
|
|
132
132
|
dom[call](node, parent, head, source);
|
|
133
133
|
break;
|
|
134
134
|
case "BASE":
|
|
@@ -138,7 +138,7 @@ export default function (node: Node, source: Source): Node {
|
|
|
138
138
|
// We create "a" element so we can generate protocol and hostname for relative paths like "/path/"
|
|
139
139
|
let a = document.createElement("a");
|
|
140
140
|
a.href = attributes["href"];
|
|
141
|
-
baseHead.data.attributes[Constant.Base] = a.protocol + "//" + a.
|
|
141
|
+
baseHead.data.attributes[Constant.Base] = a.protocol + "//" + a.host + a.pathname;
|
|
142
142
|
}
|
|
143
143
|
break;
|
|
144
144
|
case "STYLE":
|
package/types/core.d.ts
CHANGED
|
@@ -5,7 +5,6 @@ type TaskResolve = () => void;
|
|
|
5
5
|
type UploadCallback = (data: string) => void;
|
|
6
6
|
type Region = [number /* RegionId */, string /* Query Selector */];
|
|
7
7
|
type Checksum = [number /* FraudId */, string /* Query Selector */];
|
|
8
|
-
export type Extract = ExtractSource /* Extraction Source */ | number /* Extract Id */ | string | string[] /* Hash or Query Selector or String Token */;
|
|
9
8
|
|
|
10
9
|
/* Enum */
|
|
11
10
|
|
|
@@ -127,7 +126,6 @@ export interface Config {
|
|
|
127
126
|
mask?: string[];
|
|
128
127
|
unmask?: string[];
|
|
129
128
|
regions?: Region[];
|
|
130
|
-
extract?: Extract[];
|
|
131
129
|
cookies?: string[];
|
|
132
130
|
fraud?: boolean;
|
|
133
131
|
checksum?: Checksum[];
|
package/types/data.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Time } from "@clarity-types/core";
|
|
2
2
|
export type Target = (number | Node);
|
|
3
|
-
export type Token = (string | number | number[] | string[]);
|
|
3
|
+
export type Token = (string | number | number[] | string[] | (string | number)[]);
|
|
4
4
|
export type DecodedToken = (any | any[]);
|
|
5
5
|
|
|
6
6
|
export type MetadataCallback = (data: Metadata, playback: boolean) => void;
|
|
@@ -217,7 +217,8 @@ export const enum Setting {
|
|
|
217
217
|
MinUploadDelay = 100, // Minimum time before we are ready to flush events to the server
|
|
218
218
|
MaxUploadDelay = 30 * Time.Second, // Do flush out payload once every 30s,
|
|
219
219
|
ExtractLimit = 10000, // Do not extract more than 10000 characters
|
|
220
|
-
ChecksumPrecision = 24 // n-bit integer to represent token hash
|
|
220
|
+
ChecksumPrecision = 24, // n-bit integer to represent token hash
|
|
221
|
+
UploadTimeout = 15000 // Timeout in ms for XHR requests
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
export const enum Character {
|
|
@@ -263,6 +264,7 @@ export const enum Constant {
|
|
|
263
264
|
End = "END",
|
|
264
265
|
Upgrade = "UPGRADE",
|
|
265
266
|
Action = "ACTION",
|
|
267
|
+
Extract = "EXTRACT",
|
|
266
268
|
UserId = "userId",
|
|
267
269
|
SessionId = "sessionId",
|
|
268
270
|
PageId = "pageId",
|
|
@@ -284,7 +286,9 @@ export const enum Constant {
|
|
|
284
286
|
Tilde = "~",
|
|
285
287
|
ArrayStart = "[",
|
|
286
288
|
ConditionStart = "{",
|
|
287
|
-
ConditionEnd = "}"
|
|
289
|
+
ConditionEnd = "}",
|
|
290
|
+
Seperator = "<SEP>",
|
|
291
|
+
Timeout = "Timeout"
|
|
288
292
|
}
|
|
289
293
|
|
|
290
294
|
export const enum XMLReadyState {
|
|
@@ -404,7 +408,7 @@ export interface UpgradeData {
|
|
|
404
408
|
}
|
|
405
409
|
|
|
406
410
|
export interface ExtractData {
|
|
407
|
-
[key:
|
|
411
|
+
[key: number]: [number, string][]; // Array of [id, value] for every extracted data
|
|
408
412
|
}
|
|
409
413
|
|
|
410
414
|
export interface UploadData {
|