@datalyr/api 1.2.0 → 1.2.2
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/README.md +254 -103
- package/dist/index.js +3 -3
- package/dist/index.mjs +3 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,195 +1,346 @@
|
|
|
1
1
|
# @datalyr/api
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Server-side analytics and attribution SDK for Node.js. Track events, identify users, and preserve attribution data from your backend.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npm install @datalyr/api
|
|
9
|
-
# or
|
|
10
|
-
yarn add @datalyr/api
|
|
11
|
-
# or
|
|
12
|
-
pnpm add @datalyr/api
|
|
13
9
|
```
|
|
14
10
|
|
|
15
11
|
## Quick Start
|
|
16
12
|
|
|
17
13
|
```javascript
|
|
18
|
-
const { Datalyr } = require('@datalyr/api');
|
|
19
|
-
// or
|
|
20
14
|
import { Datalyr } from '@datalyr/api';
|
|
21
15
|
|
|
22
|
-
//
|
|
23
|
-
const datalyr = new Datalyr('
|
|
16
|
+
// String shorthand
|
|
17
|
+
const datalyr = new Datalyr('dk_your_api_key');
|
|
18
|
+
|
|
19
|
+
// Or with config object
|
|
20
|
+
const datalyr = new Datalyr({
|
|
21
|
+
apiKey: 'dk_your_api_key',
|
|
22
|
+
debug: true,
|
|
23
|
+
});
|
|
24
24
|
|
|
25
25
|
// Track an event
|
|
26
|
+
await datalyr.track('user_123', 'signup_completed', { plan: 'pro' });
|
|
27
|
+
|
|
28
|
+
// Identify a user
|
|
29
|
+
await datalyr.identify('user_123', { email: 'user@example.com' });
|
|
30
|
+
|
|
31
|
+
// Flush and shut down
|
|
32
|
+
await datalyr.close();
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Configuration
|
|
36
|
+
|
|
37
|
+
The constructor accepts a `DatalyrConfig` object or an API key string.
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
// Config object
|
|
41
|
+
const datalyr = new Datalyr({
|
|
42
|
+
apiKey: 'dk_...', // Required. Must start with "dk_".
|
|
43
|
+
host: 'https://ingest.datalyr.com/track', // API endpoint (default: 'https://ingest.datalyr.com/track')
|
|
44
|
+
flushAt: 20, // Flush when queue reaches this size (default: 20, range: 1-100)
|
|
45
|
+
flushInterval: 10000, // Flush timer interval in ms (default: 10000)
|
|
46
|
+
debug: false, // Log events and errors to console (default: false)
|
|
47
|
+
timeout: 10000, // HTTP request timeout in ms (default: 10000, range: 1000-60000)
|
|
48
|
+
retryLimit: 3, // Max retries for failed requests (default: 3)
|
|
49
|
+
maxQueueSize: 1000, // Max queued events before dropping oldest (default: 1000, range: 100-10000)
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// String shorthand — uses all defaults
|
|
53
|
+
const datalyr = new Datalyr('dk_your_api_key');
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Methods
|
|
57
|
+
|
|
58
|
+
### track()
|
|
59
|
+
|
|
60
|
+
Two call signatures:
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
// Object form (TrackOptions)
|
|
64
|
+
await datalyr.track({
|
|
65
|
+
event: 'Purchase Completed', // Required
|
|
66
|
+
userId: 'user_123', // Optional
|
|
67
|
+
anonymousId: 'anon_from_browser', // Optional — override the auto-generated anonymous ID
|
|
68
|
+
properties: { // Optional
|
|
69
|
+
amount: 99.99,
|
|
70
|
+
currency: 'USD',
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Legacy form
|
|
26
75
|
await datalyr.track('user_123', 'Purchase Completed', {
|
|
27
76
|
amount: 99.99,
|
|
28
77
|
currency: 'USD',
|
|
29
|
-
products: ['item_1', 'item_2']
|
|
30
78
|
});
|
|
31
79
|
|
|
32
|
-
//
|
|
80
|
+
// Pass null as userId for anonymous events
|
|
81
|
+
await datalyr.track(null, 'page_loaded', { url: '/pricing' });
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### identify()
|
|
85
|
+
|
|
86
|
+
```javascript
|
|
87
|
+
await datalyr.identify(userId: string, traits?: any);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Links a user ID to traits. Internally sends a `$identify` event.
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
33
93
|
await datalyr.identify('user_123', {
|
|
34
94
|
email: 'user@example.com',
|
|
35
|
-
name: '
|
|
36
|
-
plan: 'premium'
|
|
95
|
+
name: 'Jane Doe',
|
|
96
|
+
plan: 'premium',
|
|
37
97
|
});
|
|
98
|
+
```
|
|
38
99
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
100
|
+
### page()
|
|
101
|
+
|
|
102
|
+
```javascript
|
|
103
|
+
await datalyr.page(userId: string, name?: string, properties?: any);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Track a page view. Internally sends a `$pageview` event.
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
await datalyr.page('user_123', 'Pricing', {
|
|
110
|
+
url: 'https://example.com/pricing',
|
|
111
|
+
referrer: 'https://google.com',
|
|
43
112
|
});
|
|
113
|
+
```
|
|
44
114
|
|
|
45
|
-
|
|
115
|
+
### group()
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
await datalyr.group(userId: string, groupId: string, traits?: any);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Associate a user with a group (company, team, etc.). Internally sends a `$group` event.
|
|
122
|
+
|
|
123
|
+
```javascript
|
|
46
124
|
await datalyr.group('user_123', 'company_456', {
|
|
47
125
|
name: 'Acme Corp',
|
|
48
|
-
industry: 'Technology'
|
|
126
|
+
industry: 'Technology',
|
|
127
|
+
employees: 50,
|
|
49
128
|
});
|
|
129
|
+
```
|
|
50
130
|
|
|
51
|
-
|
|
52
|
-
|
|
131
|
+
### flush()
|
|
132
|
+
|
|
133
|
+
Send all queued events immediately.
|
|
134
|
+
|
|
135
|
+
```javascript
|
|
136
|
+
await datalyr.flush();
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### close()
|
|
140
|
+
|
|
141
|
+
Stops the flush timer, then attempts a final flush with a **5-second timeout**. Any events still queued after the timeout are dropped. New events tracked after `close()` is called are silently ignored.
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
process.on('SIGTERM', async () => {
|
|
145
|
+
await datalyr.close();
|
|
146
|
+
process.exit(0);
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### getAnonymousId()
|
|
151
|
+
|
|
152
|
+
Returns the SDK instance's persistent anonymous ID. The ID is generated lazily on first use and has the format `anon_<random><timestamp>` (e.g., `anon_k7x2m9f1lxyzabc`).
|
|
153
|
+
|
|
154
|
+
```javascript
|
|
155
|
+
const anonId = datalyr.getAnonymousId();
|
|
53
156
|
```
|
|
54
157
|
|
|
55
|
-
##
|
|
158
|
+
## Event Payload
|
|
56
159
|
|
|
57
|
-
|
|
160
|
+
Every event sent to the API has this structure:
|
|
161
|
+
|
|
162
|
+
```javascript
|
|
163
|
+
{
|
|
164
|
+
event: 'Purchase Completed',
|
|
165
|
+
userId: 'user_123', // undefined if not provided
|
|
166
|
+
anonymousId: 'anon_k7x2m9f...', // Always present
|
|
167
|
+
properties: {
|
|
168
|
+
amount: 99.99,
|
|
169
|
+
anonymous_id: 'anon_k7x2m9f...', // Automatically added
|
|
170
|
+
},
|
|
171
|
+
context: {
|
|
172
|
+
library: '@datalyr/api',
|
|
173
|
+
version: '1.2.1',
|
|
174
|
+
source: 'api',
|
|
175
|
+
},
|
|
176
|
+
timestamp: '2025-01-15T10:30:00.000Z',
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Notes:
|
|
181
|
+
- `anonymous_id` is automatically added to `properties` on every event for attribution.
|
|
182
|
+
- The `context` object identifies the SDK and version.
|
|
183
|
+
- `timestamp` is set to the ISO 8601 time when the event was created.
|
|
184
|
+
|
|
185
|
+
## Batching and Retry Behavior
|
|
186
|
+
|
|
187
|
+
Events are queued locally and sent in batches, not one at a time.
|
|
188
|
+
|
|
189
|
+
- **Auto-flush triggers:** when the queue reaches `flushAt` events, or every `flushInterval` ms.
|
|
190
|
+
- **Batch size:** events are sent in parallel batches of 10 within a single flush.
|
|
191
|
+
- **Queue overflow:** when the queue reaches `maxQueueSize`, the oldest event is dropped to make room.
|
|
192
|
+
- **Retry:** 5xx (server) errors are retried up to `retryLimit` times with exponential backoff (1s, 2s, 4s, ... capped at 10s). 4xx (client) errors are permanent failures and are not retried.
|
|
193
|
+
- **Failed events:** events that fail after all retries are re-queued at the front.
|
|
194
|
+
|
|
195
|
+
## Attribution Preservation
|
|
196
|
+
|
|
197
|
+
Pass the anonymous ID from your browser or mobile SDK to link server-side events to a client-side session:
|
|
58
198
|
|
|
59
199
|
```javascript
|
|
60
|
-
// Option 1: Pass anonymous_id from browser/mobile for attribution preservation
|
|
61
200
|
await datalyr.track({
|
|
62
201
|
event: 'Purchase Completed',
|
|
63
202
|
userId: 'user_123',
|
|
64
|
-
anonymousId: req.body.anonymous_id, // From browser
|
|
203
|
+
anonymousId: req.body.anonymous_id, // From browser SDK
|
|
65
204
|
properties: {
|
|
66
205
|
amount: 99.99,
|
|
67
|
-
|
|
68
|
-
}
|
|
206
|
+
},
|
|
69
207
|
});
|
|
208
|
+
```
|
|
70
209
|
|
|
71
|
-
|
|
72
|
-
await datalyr.track('user_123', 'Purchase Completed', {
|
|
73
|
-
amount: 99.99
|
|
74
|
-
});
|
|
210
|
+
This preserves UTM parameters, click IDs (gclid, fbclid, ttclid), referrer, landing page, and the full customer journey.
|
|
75
211
|
|
|
76
|
-
|
|
77
|
-
const anonymousId = datalyr.getAnonymousId();
|
|
78
|
-
```
|
|
212
|
+
## Framework Examples
|
|
79
213
|
|
|
80
|
-
### Express.js
|
|
214
|
+
### Express.js Middleware
|
|
81
215
|
|
|
82
216
|
```javascript
|
|
217
|
+
import express from 'express';
|
|
218
|
+
import { Datalyr } from '@datalyr/api';
|
|
219
|
+
|
|
220
|
+
const app = express();
|
|
221
|
+
const datalyr = new Datalyr('dk_your_api_key');
|
|
222
|
+
|
|
83
223
|
app.post('/api/purchase', async (req, res) => {
|
|
84
|
-
const { items, anonymous_id } = req.body;
|
|
85
|
-
|
|
86
|
-
// Track with anonymous_id to preserve attribution (fbclid, gclid, etc.)
|
|
224
|
+
const { items, anonymous_id } = req.body;
|
|
225
|
+
|
|
87
226
|
await datalyr.track({
|
|
88
227
|
event: 'Purchase Completed',
|
|
89
228
|
userId: req.user?.id,
|
|
90
|
-
anonymousId: anonymous_id,
|
|
229
|
+
anonymousId: anonymous_id,
|
|
91
230
|
properties: {
|
|
92
231
|
total: calculateTotal(items),
|
|
93
|
-
|
|
94
|
-
}
|
|
232
|
+
item_count: items.length,
|
|
233
|
+
},
|
|
95
234
|
});
|
|
96
|
-
|
|
235
|
+
|
|
97
236
|
res.json({ success: true });
|
|
98
237
|
});
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
### Key Benefits:
|
|
102
|
-
- **Attribution Preservation**: Never lose fbclid, gclid, ttclid, or lyr tracking
|
|
103
|
-
- **Complete Journey**: Track users from web → server → mobile
|
|
104
|
-
- **Flexible API**: Support both legacy and new tracking methods
|
|
105
238
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const datalyr = new Datalyr({
|
|
110
|
-
apiKey: 'your_api_key_here',
|
|
111
|
-
host: 'https://api.datalyr.com', // Optional: custom host
|
|
112
|
-
flushAt: 20, // Optional: batch size (default: 20)
|
|
113
|
-
flushInterval: 10000, // Optional: batch interval in ms (default: 10000)
|
|
114
|
-
debug: true, // Optional: enable debug logging (default: false)
|
|
115
|
-
timeout: 10000, // Optional: request timeout in ms (default: 10000)
|
|
116
|
-
retryLimit: 3, // Optional: max retries (default: 3)
|
|
117
|
-
maxQueueSize: 1000 // Optional: max events in queue (default: 1000)
|
|
239
|
+
process.on('SIGTERM', async () => {
|
|
240
|
+
await datalyr.close();
|
|
241
|
+
process.exit(0);
|
|
118
242
|
});
|
|
119
243
|
```
|
|
120
244
|
|
|
121
|
-
|
|
245
|
+
### Stripe Webhooks
|
|
122
246
|
|
|
123
247
|
```javascript
|
|
124
|
-
|
|
125
|
-
|
|
248
|
+
import { Datalyr } from '@datalyr/api';
|
|
249
|
+
import Stripe from 'stripe';
|
|
126
250
|
|
|
127
|
-
const datalyr = new Datalyr(
|
|
251
|
+
const datalyr = new Datalyr('dk_your_api_key');
|
|
252
|
+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
|
|
128
253
|
|
|
129
254
|
app.post('/webhooks/stripe', async (req, res) => {
|
|
130
255
|
const sig = req.headers['stripe-signature'];
|
|
131
256
|
const event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
|
|
132
|
-
|
|
257
|
+
|
|
133
258
|
switch (event.type) {
|
|
134
|
-
case 'checkout.session.completed':
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
stripeSessionId: event.data.object.id
|
|
142
|
-
}
|
|
143
|
-
);
|
|
259
|
+
case 'checkout.session.completed': {
|
|
260
|
+
const session = event.data.object;
|
|
261
|
+
await datalyr.track(session.client_reference_id, 'Purchase Completed', {
|
|
262
|
+
amount: session.amount_total / 100,
|
|
263
|
+
currency: session.currency,
|
|
264
|
+
stripe_session_id: session.id,
|
|
265
|
+
});
|
|
144
266
|
break;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
case 'customer.subscription.created': {
|
|
270
|
+
const subscription = event.data.object;
|
|
271
|
+
await datalyr.track(subscription.metadata.userId, 'Subscription Started', {
|
|
272
|
+
plan: subscription.items.data[0].price.nickname,
|
|
273
|
+
mrr: subscription.items.data[0].price.unit_amount / 100,
|
|
274
|
+
interval: subscription.items.data[0].price.recurring.interval,
|
|
275
|
+
});
|
|
156
276
|
break;
|
|
277
|
+
}
|
|
157
278
|
}
|
|
158
|
-
|
|
279
|
+
|
|
159
280
|
res.json({ received: true });
|
|
160
281
|
});
|
|
161
282
|
```
|
|
162
283
|
|
|
163
|
-
##
|
|
284
|
+
## TypeScript
|
|
164
285
|
|
|
165
|
-
|
|
286
|
+
Full type definitions are included. Exported types:
|
|
166
287
|
|
|
167
|
-
|
|
288
|
+
```typescript
|
|
289
|
+
import { Datalyr, DatalyrConfig, TrackOptions, TrackEvent } from '@datalyr/api';
|
|
168
290
|
|
|
169
|
-
|
|
291
|
+
const config: DatalyrConfig = {
|
|
292
|
+
apiKey: 'dk_your_api_key',
|
|
293
|
+
debug: true,
|
|
294
|
+
};
|
|
170
295
|
|
|
171
|
-
|
|
296
|
+
const datalyr = new Datalyr(config);
|
|
172
297
|
|
|
173
|
-
|
|
298
|
+
const options: TrackOptions = {
|
|
299
|
+
event: 'Purchase Completed',
|
|
300
|
+
userId: 'user_123',
|
|
301
|
+
anonymousId: 'anon_from_browser',
|
|
302
|
+
properties: {
|
|
303
|
+
amount: 99.99,
|
|
304
|
+
currency: 'USD',
|
|
305
|
+
},
|
|
306
|
+
};
|
|
174
307
|
|
|
175
|
-
|
|
308
|
+
await datalyr.track(options);
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Troubleshooting
|
|
176
312
|
|
|
177
|
-
|
|
313
|
+
**Events not appearing**
|
|
178
314
|
|
|
179
|
-
|
|
315
|
+
1. Verify your API key starts with `dk_`.
|
|
316
|
+
2. Enable `debug: true` to see console output.
|
|
317
|
+
3. Call `await datalyr.flush()` to force-send queued events.
|
|
318
|
+
4. Check for 4xx errors in debug output -- these indicate a client-side issue (bad API key, malformed payload).
|
|
180
319
|
|
|
181
|
-
|
|
320
|
+
**Request timeouts**
|
|
182
321
|
|
|
183
|
-
|
|
322
|
+
Increase `timeout` and `retryLimit`:
|
|
184
323
|
|
|
185
|
-
|
|
324
|
+
```javascript
|
|
325
|
+
const datalyr = new Datalyr({
|
|
326
|
+
apiKey: 'dk_your_api_key',
|
|
327
|
+
timeout: 30000,
|
|
328
|
+
retryLimit: 5,
|
|
329
|
+
});
|
|
330
|
+
```
|
|
186
331
|
|
|
187
|
-
|
|
332
|
+
**Queue full (oldest events dropped)**
|
|
188
333
|
|
|
189
|
-
|
|
334
|
+
Increase `maxQueueSize` or flush more aggressively:
|
|
190
335
|
|
|
191
|
-
|
|
336
|
+
```javascript
|
|
337
|
+
const datalyr = new Datalyr({
|
|
338
|
+
apiKey: 'dk_your_api_key',
|
|
339
|
+
maxQueueSize: 5000,
|
|
340
|
+
flushAt: 50,
|
|
341
|
+
});
|
|
342
|
+
```
|
|
192
343
|
|
|
193
344
|
## License
|
|
194
345
|
|
|
195
|
-
MIT
|
|
346
|
+
MIT
|
package/dist/index.js
CHANGED
|
@@ -32,7 +32,7 @@ var Datalyr = class {
|
|
|
32
32
|
this.isClosing = false;
|
|
33
33
|
if (typeof config === "string") {
|
|
34
34
|
this.apiKey = config;
|
|
35
|
-
this.host = "https://
|
|
35
|
+
this.host = "https://ingest.datalyr.com/track";
|
|
36
36
|
this.debug = false;
|
|
37
37
|
this.flushAt = 20;
|
|
38
38
|
this.flushInterval = 1e4;
|
|
@@ -41,7 +41,7 @@ var Datalyr = class {
|
|
|
41
41
|
this.maxQueueSize = 1e3;
|
|
42
42
|
} else {
|
|
43
43
|
this.apiKey = config.apiKey;
|
|
44
|
-
this.host = config.host || "https://
|
|
44
|
+
this.host = config.host || "https://ingest.datalyr.com/track";
|
|
45
45
|
this.debug = config.debug || false;
|
|
46
46
|
this.flushAt = config.flushAt || 20;
|
|
47
47
|
this.flushInterval = config.flushInterval || 1e4;
|
|
@@ -100,7 +100,7 @@ var Datalyr = class {
|
|
|
100
100
|
properties: enrichedProperties,
|
|
101
101
|
context: {
|
|
102
102
|
library: "@datalyr/api",
|
|
103
|
-
version: "1.1
|
|
103
|
+
version: "1.2.1",
|
|
104
104
|
source: "api"
|
|
105
105
|
// Explicitly set source for server-side API
|
|
106
106
|
},
|
package/dist/index.mjs
CHANGED
|
@@ -7,7 +7,7 @@ var Datalyr = class {
|
|
|
7
7
|
this.isClosing = false;
|
|
8
8
|
if (typeof config === "string") {
|
|
9
9
|
this.apiKey = config;
|
|
10
|
-
this.host = "https://
|
|
10
|
+
this.host = "https://ingest.datalyr.com/track";
|
|
11
11
|
this.debug = false;
|
|
12
12
|
this.flushAt = 20;
|
|
13
13
|
this.flushInterval = 1e4;
|
|
@@ -16,7 +16,7 @@ var Datalyr = class {
|
|
|
16
16
|
this.maxQueueSize = 1e3;
|
|
17
17
|
} else {
|
|
18
18
|
this.apiKey = config.apiKey;
|
|
19
|
-
this.host = config.host || "https://
|
|
19
|
+
this.host = config.host || "https://ingest.datalyr.com/track";
|
|
20
20
|
this.debug = config.debug || false;
|
|
21
21
|
this.flushAt = config.flushAt || 20;
|
|
22
22
|
this.flushInterval = config.flushInterval || 1e4;
|
|
@@ -75,7 +75,7 @@ var Datalyr = class {
|
|
|
75
75
|
properties: enrichedProperties,
|
|
76
76
|
context: {
|
|
77
77
|
library: "@datalyr/api",
|
|
78
|
-
version: "1.1
|
|
78
|
+
version: "1.2.1",
|
|
79
79
|
source: "api"
|
|
80
80
|
// Explicitly set source for server-side API
|
|
81
81
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datalyr/api",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Datalyr API SDK for server-side tracking",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -43,4 +43,4 @@
|
|
|
43
43
|
"engines": {
|
|
44
44
|
"node": ">=14.0.0"
|
|
45
45
|
}
|
|
46
|
-
}
|
|
46
|
+
}
|