@openreplay/tracker 12.0.11 → 12.1.0-beta.99
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/CHANGELOG.md +4 -0
- package/cjs/app/index.d.ts +7 -6
- package/cjs/app/index.js +81 -92
- package/cjs/app/messages.gen.d.ts +2 -2
- package/cjs/app/messages.gen.js +8 -8
- package/cjs/app/workerManager/QueueSender.d.ts +25 -0
- package/cjs/app/workerManager/QueueSender.js +133 -0
- package/cjs/app/workerManager/index.d.ts +37 -0
- package/cjs/app/workerManager/index.js +166 -0
- package/cjs/common/interaction.d.ts +56 -21
- package/cjs/common/messages.gen.d.ts +7 -7
- package/cjs/index.js +1 -1
- package/lib/app/index.d.ts +7 -6
- package/lib/app/index.js +81 -92
- package/lib/app/messages.gen.d.ts +2 -2
- package/lib/app/messages.gen.js +4 -4
- package/lib/app/workerManager/QueueSender.d.ts +25 -0
- package/lib/app/workerManager/QueueSender.js +130 -0
- package/lib/app/workerManager/index.d.ts +37 -0
- package/lib/app/workerManager/index.js +161 -0
- package/lib/common/interaction.d.ts +56 -21
- package/lib/common/messages.gen.d.ts +7 -7
- package/lib/common/tsconfig.tsbuildinfo +1 -1
- package/lib/index.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { FromWorkerData, ToWorkerData, WorkerAuth, WorkerStart } from '../../common/interaction.js';
|
|
2
|
+
import App from '../index.js';
|
|
3
|
+
import QueueSender from './QueueSender.js';
|
|
4
|
+
declare enum WorkerStatus {
|
|
5
|
+
NotActive = 0,
|
|
6
|
+
Starting = 1,
|
|
7
|
+
Stopping = 2,
|
|
8
|
+
Active = 3,
|
|
9
|
+
Stopped = 4
|
|
10
|
+
}
|
|
11
|
+
interface TypedWorker extends Omit<Worker, 'postMessage'> {
|
|
12
|
+
postMessage(data: ToWorkerData): void;
|
|
13
|
+
}
|
|
14
|
+
declare class WebWorkerManager {
|
|
15
|
+
private readonly app;
|
|
16
|
+
private readonly worker;
|
|
17
|
+
private readonly onError;
|
|
18
|
+
sendIntervalID: ReturnType<typeof setInterval> | null;
|
|
19
|
+
restartTimeoutID: ReturnType<typeof setTimeout> | null;
|
|
20
|
+
workerStatus: WorkerStatus;
|
|
21
|
+
sender: QueueSender | null;
|
|
22
|
+
constructor(app: App, worker: TypedWorker, onError: (ctx: string, e: any) => any);
|
|
23
|
+
finalize: () => void;
|
|
24
|
+
resetWebWorker: () => void;
|
|
25
|
+
resetSender: () => void;
|
|
26
|
+
reset: () => void;
|
|
27
|
+
initiateRestart: () => void;
|
|
28
|
+
initiateFailure: (reason: string) => void;
|
|
29
|
+
processMessage: (data: ToWorkerData | null) => void;
|
|
30
|
+
startWorker: (data: WorkerStart) => void;
|
|
31
|
+
stopWorker: () => void;
|
|
32
|
+
authorizeWorker: (data: WorkerAuth) => void;
|
|
33
|
+
sendCompressedBatch: (data: Uint8Array) => void;
|
|
34
|
+
sendUncompressedBatch: (data: Uint8Array) => void;
|
|
35
|
+
postMessage: (data: FromWorkerData) => void;
|
|
36
|
+
}
|
|
37
|
+
export default WebWorkerManager;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const QueueSender_js_1 = __importDefault(require("./QueueSender.js"));
|
|
7
|
+
var WorkerStatus;
|
|
8
|
+
(function (WorkerStatus) {
|
|
9
|
+
WorkerStatus[WorkerStatus["NotActive"] = 0] = "NotActive";
|
|
10
|
+
WorkerStatus[WorkerStatus["Starting"] = 1] = "Starting";
|
|
11
|
+
WorkerStatus[WorkerStatus["Stopping"] = 2] = "Stopping";
|
|
12
|
+
WorkerStatus[WorkerStatus["Active"] = 3] = "Active";
|
|
13
|
+
WorkerStatus[WorkerStatus["Stopped"] = 4] = "Stopped";
|
|
14
|
+
})(WorkerStatus || (WorkerStatus = {}));
|
|
15
|
+
const AUTO_SEND_INTERVAL = 10 * 1000;
|
|
16
|
+
const rebroadcastEvents = ['queue_empty', 'not_init', 'restart'];
|
|
17
|
+
class WebWorkerManager {
|
|
18
|
+
constructor(app, worker, onError) {
|
|
19
|
+
this.app = app;
|
|
20
|
+
this.worker = worker;
|
|
21
|
+
this.onError = onError;
|
|
22
|
+
this.sendIntervalID = null;
|
|
23
|
+
this.restartTimeoutID = null;
|
|
24
|
+
this.workerStatus = WorkerStatus.NotActive;
|
|
25
|
+
this.sender = null;
|
|
26
|
+
this.finalize = () => {
|
|
27
|
+
this.worker.postMessage({ type: 'writer_finalize' });
|
|
28
|
+
};
|
|
29
|
+
this.resetWebWorker = () => {
|
|
30
|
+
this.worker.postMessage({ type: 'reset_writer' });
|
|
31
|
+
};
|
|
32
|
+
this.resetSender = () => {
|
|
33
|
+
if (!this.sender) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
this.sender.clean();
|
|
37
|
+
setTimeout(() => {
|
|
38
|
+
this.sender = null;
|
|
39
|
+
}, 20);
|
|
40
|
+
};
|
|
41
|
+
this.reset = () => {
|
|
42
|
+
this.workerStatus = WorkerStatus.Stopping;
|
|
43
|
+
if (this.sendIntervalID !== null) {
|
|
44
|
+
clearInterval(this.sendIntervalID);
|
|
45
|
+
this.sendIntervalID = null;
|
|
46
|
+
}
|
|
47
|
+
this.resetSender();
|
|
48
|
+
this.resetWebWorker();
|
|
49
|
+
setTimeout(() => {
|
|
50
|
+
this.workerStatus = WorkerStatus.NotActive;
|
|
51
|
+
}, 100);
|
|
52
|
+
};
|
|
53
|
+
this.initiateRestart = () => {
|
|
54
|
+
if (this.workerStatus === WorkerStatus.Stopped) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
this.postMessage({ type: 'restart' });
|
|
58
|
+
this.reset();
|
|
59
|
+
};
|
|
60
|
+
this.initiateFailure = (reason) => {
|
|
61
|
+
if ([WorkerStatus.Stopped, WorkerStatus.Stopping, WorkerStatus.NotActive].includes(this.workerStatus)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.postMessage({ type: 'failure', reason });
|
|
65
|
+
this.reset();
|
|
66
|
+
};
|
|
67
|
+
this.processMessage = (data) => {
|
|
68
|
+
if (data === null) {
|
|
69
|
+
this.finalize();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (data.type === 'batch' && Array.isArray(data.data)) {
|
|
73
|
+
data.data.forEach((message) => {
|
|
74
|
+
if (message[0] === 55 /* MType.SetPageVisibility */) {
|
|
75
|
+
// document is hidden
|
|
76
|
+
if (message[1]) {
|
|
77
|
+
this.restartTimeoutID = setTimeout(() => this.initiateRestart(), 30 * 60 * 1000);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
if (this.restartTimeoutID) {
|
|
81
|
+
clearTimeout(this.restartTimeoutID);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
this.worker.postMessage({ type: 'to_writer', data: data.data });
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
this.startWorker = (data) => {
|
|
90
|
+
this.sender = new QueueSender_js_1.default(data.ingestPoint, () => {
|
|
91
|
+
// onUnauthorised
|
|
92
|
+
this.initiateRestart();
|
|
93
|
+
}, (reason) => {
|
|
94
|
+
// onFailure
|
|
95
|
+
this.initiateFailure(reason);
|
|
96
|
+
}, data.connAttemptCount, data.connAttemptGap, (batch) => {
|
|
97
|
+
this.postMessage({ type: 'compress', batch });
|
|
98
|
+
});
|
|
99
|
+
if (this.sendIntervalID === null) {
|
|
100
|
+
this.sendIntervalID = setInterval(this.finalize, AUTO_SEND_INTERVAL);
|
|
101
|
+
}
|
|
102
|
+
this.worker.postMessage(data);
|
|
103
|
+
return;
|
|
104
|
+
};
|
|
105
|
+
this.stopWorker = () => {
|
|
106
|
+
this.finalize();
|
|
107
|
+
this.reset();
|
|
108
|
+
return;
|
|
109
|
+
};
|
|
110
|
+
this.authorizeWorker = (data) => {
|
|
111
|
+
if (!this.sender) {
|
|
112
|
+
console.debug('OR WebWorker: sender not initialised. Received auth.');
|
|
113
|
+
this.initiateRestart();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.sender.authorise(data.token);
|
|
117
|
+
if (data.beaconSizeLimit) {
|
|
118
|
+
this.worker.postMessage({ type: 'beacon_size_limit', data: data.beaconSizeLimit });
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
};
|
|
122
|
+
this.sendCompressedBatch = (data) => {
|
|
123
|
+
if (!this.sender) {
|
|
124
|
+
console.debug('OR Worker: sender not init. Compressed batch');
|
|
125
|
+
this.initiateRestart();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this.sender?.sendCompressed(data);
|
|
129
|
+
return;
|
|
130
|
+
};
|
|
131
|
+
this.sendUncompressedBatch = (data) => {
|
|
132
|
+
if (!this.sender) {
|
|
133
|
+
console.debug('OR Worker: sender not init. Compressed batch.');
|
|
134
|
+
this.initiateRestart();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (data) {
|
|
138
|
+
this.sender.sendUncompressed(data);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
this.postMessage = (data) => {
|
|
143
|
+
this.app.handleWorkerMsg(data);
|
|
144
|
+
};
|
|
145
|
+
this.worker.onerror = (e) => {
|
|
146
|
+
this.onError('webworker_error', e);
|
|
147
|
+
};
|
|
148
|
+
this.worker.onmessage = ({ data }) => {
|
|
149
|
+
if (rebroadcastEvents.includes(data.type)) {
|
|
150
|
+
this.postMessage(data);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
switch (data.type) {
|
|
154
|
+
case 'status':
|
|
155
|
+
this.workerStatus = data.data;
|
|
156
|
+
return;
|
|
157
|
+
case 'batch_ready':
|
|
158
|
+
if (this.sender) {
|
|
159
|
+
this.app.debug.log('Openreplay: msg batch to sender: ', data.data);
|
|
160
|
+
this.sender.push(data.data);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
exports.default = WebWorkerManager;
|
|
@@ -3,7 +3,61 @@ export interface Options {
|
|
|
3
3
|
connAttemptCount?: number;
|
|
4
4
|
connAttemptGap?: number;
|
|
5
5
|
}
|
|
6
|
-
type
|
|
6
|
+
export type ToWorkerData = null | Stop | Batch | WorkerStart | BeaconSizeLimit | ToWriterData | ForceFlushBatch | CheckQueue | ResetWriter | WriterFinalize;
|
|
7
|
+
export type FromWorkerData = Restart | Failure | NotInit | Compress | QEmpty | Status | BatchReady;
|
|
8
|
+
type BatchReady = {
|
|
9
|
+
type: 'batch_ready';
|
|
10
|
+
data: Uint8Array;
|
|
11
|
+
};
|
|
12
|
+
type Status = {
|
|
13
|
+
type: 'status';
|
|
14
|
+
data: number;
|
|
15
|
+
};
|
|
16
|
+
type Compress = {
|
|
17
|
+
type: 'compress';
|
|
18
|
+
batch: Uint8Array;
|
|
19
|
+
};
|
|
20
|
+
type Restart = {
|
|
21
|
+
type: 'restart';
|
|
22
|
+
};
|
|
23
|
+
type NotInit = {
|
|
24
|
+
type: 'not_init';
|
|
25
|
+
};
|
|
26
|
+
type Stop = {
|
|
27
|
+
type: 'stop';
|
|
28
|
+
};
|
|
29
|
+
type Batch = {
|
|
30
|
+
type: 'batch';
|
|
31
|
+
data: Array<Message>;
|
|
32
|
+
};
|
|
33
|
+
type ForceFlushBatch = {
|
|
34
|
+
type: 'forceFlushBatch';
|
|
35
|
+
};
|
|
36
|
+
type CheckQueue = {
|
|
37
|
+
type: 'check_queue';
|
|
38
|
+
};
|
|
39
|
+
type WriterFinalize = {
|
|
40
|
+
type: 'writer_finalize';
|
|
41
|
+
};
|
|
42
|
+
type ResetWriter = {
|
|
43
|
+
type: 'reset_writer';
|
|
44
|
+
};
|
|
45
|
+
type BeaconSizeLimit = {
|
|
46
|
+
type: 'beacon_size_limit';
|
|
47
|
+
data: number;
|
|
48
|
+
};
|
|
49
|
+
type ToWriterData = {
|
|
50
|
+
type: 'to_writer';
|
|
51
|
+
data: Array<Message>;
|
|
52
|
+
};
|
|
53
|
+
type Failure = {
|
|
54
|
+
type: 'failure';
|
|
55
|
+
reason: string;
|
|
56
|
+
};
|
|
57
|
+
type QEmpty = {
|
|
58
|
+
type: 'queue_empty';
|
|
59
|
+
};
|
|
60
|
+
export type WorkerStart = {
|
|
7
61
|
type: 'start';
|
|
8
62
|
ingestPoint: string;
|
|
9
63
|
pageNo: number;
|
|
@@ -11,27 +65,8 @@ type Start = {
|
|
|
11
65
|
url: string;
|
|
12
66
|
tabId: string;
|
|
13
67
|
} & Options;
|
|
14
|
-
type
|
|
15
|
-
type: 'auth';
|
|
68
|
+
export type WorkerAuth = {
|
|
16
69
|
token: string;
|
|
17
70
|
beaconSizeLimit?: number;
|
|
18
71
|
};
|
|
19
|
-
export type ToWorkerData = null | 'stop' | Start | Auth | Array<Message> | {
|
|
20
|
-
type: 'compressed';
|
|
21
|
-
batch: Uint8Array;
|
|
22
|
-
} | {
|
|
23
|
-
type: 'uncompressed';
|
|
24
|
-
batch: Uint8Array;
|
|
25
|
-
} | 'forceFlushBatch' | 'check_queue';
|
|
26
|
-
type Failure = {
|
|
27
|
-
type: 'failure';
|
|
28
|
-
reason: string;
|
|
29
|
-
};
|
|
30
|
-
type QEmpty = {
|
|
31
|
-
type: 'queue_empty';
|
|
32
|
-
};
|
|
33
|
-
export type FromWorkerData = 'a_stop' | 'a_start' | Failure | 'not_init' | {
|
|
34
|
-
type: 'compress';
|
|
35
|
-
batch: Uint8Array;
|
|
36
|
-
} | QEmpty;
|
|
37
72
|
export {};
|
|
@@ -30,7 +30,7 @@ export declare const enum Type {
|
|
|
30
30
|
Profiler = 40,
|
|
31
31
|
OTable = 41,
|
|
32
32
|
StateAction = 42,
|
|
33
|
-
|
|
33
|
+
Redux = 44,
|
|
34
34
|
Vuex = 45,
|
|
35
35
|
MobX = 46,
|
|
36
36
|
NgRx = 47,
|
|
@@ -71,7 +71,7 @@ export declare const enum Type {
|
|
|
71
71
|
TabData = 118,
|
|
72
72
|
CanvasNode = 119,
|
|
73
73
|
TagTrigger = 120,
|
|
74
|
-
|
|
74
|
+
ReduxNew = 121
|
|
75
75
|
}
|
|
76
76
|
export type Timestamp = [
|
|
77
77
|
Type.Timestamp,
|
|
@@ -252,8 +252,8 @@ export type StateAction = [
|
|
|
252
252
|
Type.StateAction,
|
|
253
253
|
string
|
|
254
254
|
];
|
|
255
|
-
export type
|
|
256
|
-
Type.
|
|
255
|
+
export type Redux = [
|
|
256
|
+
Type.Redux,
|
|
257
257
|
string,
|
|
258
258
|
string,
|
|
259
259
|
number
|
|
@@ -509,12 +509,12 @@ export type TagTrigger = [
|
|
|
509
509
|
Type.TagTrigger,
|
|
510
510
|
number
|
|
511
511
|
];
|
|
512
|
-
export type
|
|
513
|
-
Type.
|
|
512
|
+
export type ReduxNew = [
|
|
513
|
+
Type.ReduxNew,
|
|
514
514
|
string,
|
|
515
515
|
string,
|
|
516
516
|
number,
|
|
517
517
|
number
|
|
518
518
|
];
|
|
519
|
-
type Message = Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | NetworkRequestDeprecated | ConsoleLog | PageLoadTiming | PageRenderTiming | CustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction |
|
|
519
|
+
type Message = Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | NetworkRequestDeprecated | ConsoleLog | PageLoadTiming | PageRenderTiming | CustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | Redux | Vuex | MobX | NgRx | GraphQL | PerformanceTrack | StringDict | SetNodeAttributeDict | ResourceTimingDeprecated | ConnectionInformation | SetPageVisibility | LoadFontFace | SetNodeFocus | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner | JSException | Zustand | BatchMetadata | PartitionedMessage | NetworkRequest | WSChannel | InputChange | SelectionChange | MouseThrashing | UnbindNodes | ResourceTiming | TabChange | TabData | CanvasNode | TagTrigger | ReduxNew;
|
|
520
520
|
export default Message;
|
package/cjs/index.js
CHANGED
|
@@ -97,7 +97,7 @@ class API {
|
|
|
97
97
|
const orig = this.options.ingestPoint || index_js_1.DEFAULT_INGEST_POINT;
|
|
98
98
|
req.open('POST', orig + '/v1/web/not-started');
|
|
99
99
|
req.send(JSON.stringify({
|
|
100
|
-
trackerVersion: '12.0.
|
|
100
|
+
trackerVersion: '12.1.0-beta.99',
|
|
101
101
|
projectKey: this.options.projectKey,
|
|
102
102
|
doNotTrack,
|
|
103
103
|
reason: missingApi.length ? `missing api: ${missingApi.join(',')}` : reason,
|
package/lib/app/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { Options as ObserverOptions } from './observer/top_observer.js';
|
|
|
11
11
|
import type { Options as SanitizerOptions } from './sanitizer.js';
|
|
12
12
|
import type { Options as SessOptions } from './session.js';
|
|
13
13
|
import type { Options as NetworkOptions } from '../modules/network.js';
|
|
14
|
-
import type { Options as WebworkerOptions } from '../common/interaction.js';
|
|
14
|
+
import type { Options as WebworkerOptions, FromWorkerData } from '../common/interaction.js';
|
|
15
15
|
export interface StartOptions {
|
|
16
16
|
userID?: string;
|
|
17
17
|
metadata?: Record<string, string>;
|
|
@@ -90,7 +90,7 @@ export default class App {
|
|
|
90
90
|
private readonly revID;
|
|
91
91
|
private activityState;
|
|
92
92
|
private readonly version;
|
|
93
|
-
private readonly
|
|
93
|
+
private readonly workerManager?;
|
|
94
94
|
private compressionThreshold;
|
|
95
95
|
private restartAttempts;
|
|
96
96
|
private readonly bc;
|
|
@@ -100,9 +100,10 @@ export default class App {
|
|
|
100
100
|
private uxtManager;
|
|
101
101
|
private conditionsManager;
|
|
102
102
|
featureFlags: FeatureFlags;
|
|
103
|
-
private tagWatcher;
|
|
103
|
+
private readonly tagWatcher;
|
|
104
104
|
constructor(projectKey: string, sessionToken: string | undefined, options: Partial<Options>, signalError: (error: string, apis: string[]) => void);
|
|
105
|
-
|
|
105
|
+
handleWorkerMsg(data: FromWorkerData): void;
|
|
106
|
+
private readonly _debug;
|
|
106
107
|
private _usingOldFetchPlugin;
|
|
107
108
|
send(message: Message, urgent?: boolean): void;
|
|
108
109
|
/**
|
|
@@ -160,7 +161,7 @@ export default class App {
|
|
|
160
161
|
private checkSessionToken;
|
|
161
162
|
/**
|
|
162
163
|
* start buffering messages without starting the actual session, which gives
|
|
163
|
-
* user 30 seconds to "activate" and record session by calling `start()` on conditional trigger
|
|
164
|
+
* user 30 seconds to "activate" and record session by calling `start()` on conditional trigger,
|
|
164
165
|
* and we will then send buffered batch, so it won't get lost
|
|
165
166
|
* */
|
|
166
167
|
coldStart(startOpts?: StartOptions, conditional?: boolean): Promise<void>;
|
|
@@ -178,7 +179,7 @@ export default class App {
|
|
|
178
179
|
/**
|
|
179
180
|
* Saves the captured messages in localStorage (or whatever is used in its place)
|
|
180
181
|
*
|
|
181
|
-
* Then when this.offlineRecording is called, it will preload this messages and clear the storage item
|
|
182
|
+
* Then, when this.offlineRecording is called, it will preload this messages and clear the storage item
|
|
182
183
|
*
|
|
183
184
|
* Keeping the size of local storage reasonable is up to the end users of this library
|
|
184
185
|
* */
|