autotel-subscribers 10.0.0 → 11.0.0
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 +68 -13
- package/dist/{event-subscriber-base-CnF3V56W.d.cts → event-subscriber-base-uT-C_zrL.d.cts} +39 -3
- package/dist/{event-subscriber-base-CnF3V56W.d.ts → event-subscriber-base-uT-C_zrL.d.ts} +39 -3
- package/dist/factories.cjs +116 -13
- package/dist/factories.cjs.map +1 -1
- package/dist/factories.d.cts +1 -1
- package/dist/factories.d.ts +1 -1
- package/dist/factories.js +116 -13
- package/dist/factories.js.map +1 -1
- package/dist/index.cjs +116 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +116 -13
- package/dist/index.js.map +1 -1
- package/dist/posthog.cjs +116 -13
- package/dist/posthog.cjs.map +1 -1
- package/dist/posthog.d.cts +91 -2
- package/dist/posthog.d.ts +91 -2
- package/dist/posthog.js +116 -13
- package/dist/posthog.js.map +1 -1
- package/dist/slack.cjs +54 -0
- package/dist/slack.cjs.map +1 -1
- package/dist/slack.d.cts +1 -1
- package/dist/slack.d.ts +1 -1
- package/dist/slack.js +54 -0
- package/dist/slack.js.map +1 -1
- package/package.json +3 -3
- package/src/event-subscriber-base.ts +83 -3
- package/src/posthog.test.ts +2 -2
- package/src/posthog.ts +184 -20
package/README.md
CHANGED
|
@@ -191,12 +191,56 @@ const events = new Event('checkout', {
|
|
|
191
191
|
});
|
|
192
192
|
|
|
193
193
|
// Sent to: OpenTelemetry + PostHog
|
|
194
|
-
events.trackEvent('order.completed', {
|
|
195
|
-
userId: '123',
|
|
196
|
-
amount: 99.99
|
|
194
|
+
events.trackEvent('order.completed', {
|
|
195
|
+
userId: '123',
|
|
196
|
+
amount: 99.99
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Serverless Configuration (AWS Lambda, Vercel, Next.js):**
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
const subscriber = new PostHogSubscriber({
|
|
204
|
+
apiKey: 'phc_...',
|
|
205
|
+
serverless: true, // Auto-configures for serverless (flushAt: 1, flushInterval: 0)
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Browser Usage (with global PostHog client):**
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
// When PostHog is already loaded via script tag
|
|
213
|
+
const subscriber = new PostHogSubscriber({
|
|
214
|
+
useGlobalClient: true, // Uses window.posthog
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Advanced Options:**
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
const subscriber = new PostHogSubscriber({
|
|
222
|
+
apiKey: 'phc_...',
|
|
223
|
+
|
|
224
|
+
// Automatic filtering (enabled by default)
|
|
225
|
+
filterUndefinedValues: true, // Removes undefined/null from attributes
|
|
226
|
+
|
|
227
|
+
// Enhanced error handling
|
|
228
|
+
onErrorWithContext: (ctx) => {
|
|
229
|
+
console.error(`${ctx.eventType} failed: ${ctx.eventName}`, ctx.error);
|
|
230
|
+
Sentry.captureException(ctx.error, { extra: ctx });
|
|
231
|
+
},
|
|
197
232
|
});
|
|
198
233
|
```
|
|
199
234
|
|
|
235
|
+
**Custom Funnel Tracking:**
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
// Track custom step names (not limited to 'started'/'completed')
|
|
239
|
+
event.trackFunnelProgression('checkout', 'cart_viewed', 1);
|
|
240
|
+
event.trackFunnelProgression('checkout', 'shipping_selected', 2);
|
|
241
|
+
event.trackFunnelProgression('checkout', 'payment_entered', 3);
|
|
242
|
+
```
|
|
243
|
+
|
|
200
244
|
### Mixpanel
|
|
201
245
|
|
|
202
246
|
```typescript
|
|
@@ -357,28 +401,39 @@ All subscribers implement these methods:
|
|
|
357
401
|
```typescript
|
|
358
402
|
interface EventSubscriber {
|
|
359
403
|
// Track events
|
|
360
|
-
trackEvent(name: string, attributes?: Record<string, any>): void
|
|
361
|
-
|
|
362
|
-
// Track conversion funnels
|
|
404
|
+
trackEvent(name: string, attributes?: Record<string, any>): Promise<void>;
|
|
405
|
+
|
|
406
|
+
// Track conversion funnels (enum-based steps)
|
|
363
407
|
trackFunnelStep(
|
|
364
|
-
funnelName: string,
|
|
408
|
+
funnelName: string,
|
|
365
409
|
step: 'started' | 'completed' | 'abandoned' | 'failed',
|
|
366
410
|
attributes?: Record<string, any>
|
|
367
|
-
): void
|
|
368
|
-
|
|
411
|
+
): Promise<void>;
|
|
412
|
+
|
|
413
|
+
// Track funnel progression (custom step names)
|
|
414
|
+
trackFunnelProgression?(
|
|
415
|
+
funnelName: string,
|
|
416
|
+
stepName: string, // Any string, not limited to enum
|
|
417
|
+
stepNumber?: number, // Optional numeric position
|
|
418
|
+
attributes?: Record<string, any>
|
|
419
|
+
): Promise<void>;
|
|
420
|
+
|
|
369
421
|
// Track business outcomes
|
|
370
422
|
trackOutcome(
|
|
371
423
|
operationName: string,
|
|
372
424
|
outcome: 'success' | 'failure' | 'partial',
|
|
373
425
|
attributes?: Record<string, any>
|
|
374
|
-
): void
|
|
375
|
-
|
|
426
|
+
): Promise<void>;
|
|
427
|
+
|
|
376
428
|
// Track business values (revenue, counts, etc.)
|
|
377
429
|
trackValue(
|
|
378
|
-
name: string,
|
|
430
|
+
name: string,
|
|
379
431
|
value: number,
|
|
380
432
|
attributes?: Record<string, any>
|
|
381
|
-
): void
|
|
433
|
+
): Promise<void>;
|
|
434
|
+
|
|
435
|
+
// Flush and clean up resources
|
|
436
|
+
shutdown?(): Promise<void>;
|
|
382
437
|
}
|
|
383
438
|
```
|
|
384
439
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EventSubscriber as EventSubscriber$1, EventAttributes, FunnelStatus, OutcomeStatus } from 'autotel/event-subscriber';
|
|
1
|
+
import { EventSubscriber as EventSubscriber$1, EventAttributes, FunnelStatus, OutcomeStatus, EventAttributesInput } from 'autotel/event-subscriber';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* EventSubscriber - Standard base class for building custom subscribers
|
|
@@ -78,8 +78,12 @@ interface EventPayload {
|
|
|
78
78
|
attributes?: EventAttributes;
|
|
79
79
|
/** For funnel events: funnel name */
|
|
80
80
|
funnel?: string;
|
|
81
|
-
/** For funnel events: step status */
|
|
82
|
-
step?: FunnelStatus;
|
|
81
|
+
/** For funnel events: step status (from FunnelStatus enum) */
|
|
82
|
+
step?: FunnelStatus | string;
|
|
83
|
+
/** For funnel events: custom step name (from trackFunnelProgression) */
|
|
84
|
+
stepName?: string;
|
|
85
|
+
/** For funnel events: numeric position in funnel */
|
|
86
|
+
stepNumber?: number;
|
|
83
87
|
/** For outcome events: operation name */
|
|
84
88
|
operation?: string;
|
|
85
89
|
/** For outcome events: outcome status */
|
|
@@ -141,6 +145,26 @@ declare abstract class EventSubscriber implements EventSubscriber$1 {
|
|
|
141
145
|
* @param payload - Event payload that failed
|
|
142
146
|
*/
|
|
143
147
|
protected handleError(error: Error, payload: EventPayload): void;
|
|
148
|
+
/**
|
|
149
|
+
* Filter out undefined and null values from attributes
|
|
150
|
+
*
|
|
151
|
+
* This improves DX by allowing callers to pass objects with optional properties
|
|
152
|
+
* without having to manually filter them first.
|
|
153
|
+
*
|
|
154
|
+
* @param attributes - Input attributes (may contain undefined/null)
|
|
155
|
+
* @returns Filtered attributes with only defined values, or undefined if empty
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```typescript
|
|
159
|
+
* const filtered = this.filterAttributes({
|
|
160
|
+
* userId: user.id,
|
|
161
|
+
* email: user.email, // might be undefined
|
|
162
|
+
* plan: null, // will be filtered out
|
|
163
|
+
* });
|
|
164
|
+
* // Result: { userId: 'abc', email: 'test@example.com' } or { userId: 'abc' }
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
protected filterAttributes(attributes?: EventAttributesInput): EventAttributes | undefined;
|
|
144
168
|
/**
|
|
145
169
|
* Track an event
|
|
146
170
|
*/
|
|
@@ -157,6 +181,18 @@ declare abstract class EventSubscriber implements EventSubscriber$1 {
|
|
|
157
181
|
* Track a value/metric
|
|
158
182
|
*/
|
|
159
183
|
trackValue(name: string, value: number, attributes?: EventAttributes): Promise<void>;
|
|
184
|
+
/**
|
|
185
|
+
* Track funnel progression with custom step names
|
|
186
|
+
*
|
|
187
|
+
* Unlike trackFunnelStep which uses FunnelStatus enum values,
|
|
188
|
+
* this method allows any string as the step name for flexible funnel tracking.
|
|
189
|
+
*
|
|
190
|
+
* @param funnelName - Name of the funnel (e.g., "checkout", "onboarding")
|
|
191
|
+
* @param stepName - Custom step name (e.g., "cart_viewed", "payment_entered")
|
|
192
|
+
* @param stepNumber - Optional numeric position in the funnel
|
|
193
|
+
* @param attributes - Optional event attributes
|
|
194
|
+
*/
|
|
195
|
+
trackFunnelProgression(funnelName: string, stepName: string, stepNumber?: number, attributes?: EventAttributes): Promise<void>;
|
|
160
196
|
/**
|
|
161
197
|
* Flush pending requests and clean up
|
|
162
198
|
*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EventSubscriber as EventSubscriber$1, EventAttributes, FunnelStatus, OutcomeStatus } from 'autotel/event-subscriber';
|
|
1
|
+
import { EventSubscriber as EventSubscriber$1, EventAttributes, FunnelStatus, OutcomeStatus, EventAttributesInput } from 'autotel/event-subscriber';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* EventSubscriber - Standard base class for building custom subscribers
|
|
@@ -78,8 +78,12 @@ interface EventPayload {
|
|
|
78
78
|
attributes?: EventAttributes;
|
|
79
79
|
/** For funnel events: funnel name */
|
|
80
80
|
funnel?: string;
|
|
81
|
-
/** For funnel events: step status */
|
|
82
|
-
step?: FunnelStatus;
|
|
81
|
+
/** For funnel events: step status (from FunnelStatus enum) */
|
|
82
|
+
step?: FunnelStatus | string;
|
|
83
|
+
/** For funnel events: custom step name (from trackFunnelProgression) */
|
|
84
|
+
stepName?: string;
|
|
85
|
+
/** For funnel events: numeric position in funnel */
|
|
86
|
+
stepNumber?: number;
|
|
83
87
|
/** For outcome events: operation name */
|
|
84
88
|
operation?: string;
|
|
85
89
|
/** For outcome events: outcome status */
|
|
@@ -141,6 +145,26 @@ declare abstract class EventSubscriber implements EventSubscriber$1 {
|
|
|
141
145
|
* @param payload - Event payload that failed
|
|
142
146
|
*/
|
|
143
147
|
protected handleError(error: Error, payload: EventPayload): void;
|
|
148
|
+
/**
|
|
149
|
+
* Filter out undefined and null values from attributes
|
|
150
|
+
*
|
|
151
|
+
* This improves DX by allowing callers to pass objects with optional properties
|
|
152
|
+
* without having to manually filter them first.
|
|
153
|
+
*
|
|
154
|
+
* @param attributes - Input attributes (may contain undefined/null)
|
|
155
|
+
* @returns Filtered attributes with only defined values, or undefined if empty
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```typescript
|
|
159
|
+
* const filtered = this.filterAttributes({
|
|
160
|
+
* userId: user.id,
|
|
161
|
+
* email: user.email, // might be undefined
|
|
162
|
+
* plan: null, // will be filtered out
|
|
163
|
+
* });
|
|
164
|
+
* // Result: { userId: 'abc', email: 'test@example.com' } or { userId: 'abc' }
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
protected filterAttributes(attributes?: EventAttributesInput): EventAttributes | undefined;
|
|
144
168
|
/**
|
|
145
169
|
* Track an event
|
|
146
170
|
*/
|
|
@@ -157,6 +181,18 @@ declare abstract class EventSubscriber implements EventSubscriber$1 {
|
|
|
157
181
|
* Track a value/metric
|
|
158
182
|
*/
|
|
159
183
|
trackValue(name: string, value: number, attributes?: EventAttributes): Promise<void>;
|
|
184
|
+
/**
|
|
185
|
+
* Track funnel progression with custom step names
|
|
186
|
+
*
|
|
187
|
+
* Unlike trackFunnelStep which uses FunnelStatus enum values,
|
|
188
|
+
* this method allows any string as the step name for flexible funnel tracking.
|
|
189
|
+
*
|
|
190
|
+
* @param funnelName - Name of the funnel (e.g., "checkout", "onboarding")
|
|
191
|
+
* @param stepName - Custom step name (e.g., "cart_viewed", "payment_entered")
|
|
192
|
+
* @param stepNumber - Optional numeric position in the funnel
|
|
193
|
+
* @param attributes - Optional event attributes
|
|
194
|
+
*/
|
|
195
|
+
trackFunnelProgression(funnelName: string, stepName: string, stepNumber?: number, attributes?: EventAttributes): Promise<void>;
|
|
160
196
|
/**
|
|
161
197
|
* Flush pending requests and clean up
|
|
162
198
|
*
|
package/dist/factories.cjs
CHANGED
|
@@ -15425,6 +15425,35 @@ var EventSubscriber = class {
|
|
|
15425
15425
|
payload
|
|
15426
15426
|
);
|
|
15427
15427
|
}
|
|
15428
|
+
/**
|
|
15429
|
+
* Filter out undefined and null values from attributes
|
|
15430
|
+
*
|
|
15431
|
+
* This improves DX by allowing callers to pass objects with optional properties
|
|
15432
|
+
* without having to manually filter them first.
|
|
15433
|
+
*
|
|
15434
|
+
* @param attributes - Input attributes (may contain undefined/null)
|
|
15435
|
+
* @returns Filtered attributes with only defined values, or undefined if empty
|
|
15436
|
+
*
|
|
15437
|
+
* @example
|
|
15438
|
+
* ```typescript
|
|
15439
|
+
* const filtered = this.filterAttributes({
|
|
15440
|
+
* userId: user.id,
|
|
15441
|
+
* email: user.email, // might be undefined
|
|
15442
|
+
* plan: null, // will be filtered out
|
|
15443
|
+
* });
|
|
15444
|
+
* // Result: { userId: 'abc', email: 'test@example.com' } or { userId: 'abc' }
|
|
15445
|
+
* ```
|
|
15446
|
+
*/
|
|
15447
|
+
filterAttributes(attributes) {
|
|
15448
|
+
if (!attributes) return void 0;
|
|
15449
|
+
const filtered = {};
|
|
15450
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
15451
|
+
if (value !== void 0 && value !== null) {
|
|
15452
|
+
filtered[key] = value;
|
|
15453
|
+
}
|
|
15454
|
+
}
|
|
15455
|
+
return Object.keys(filtered).length > 0 ? filtered : void 0;
|
|
15456
|
+
}
|
|
15428
15457
|
/**
|
|
15429
15458
|
* Track an event
|
|
15430
15459
|
*/
|
|
@@ -15482,6 +15511,31 @@ var EventSubscriber = class {
|
|
|
15482
15511
|
};
|
|
15483
15512
|
await this.send(payload);
|
|
15484
15513
|
}
|
|
15514
|
+
/**
|
|
15515
|
+
* Track funnel progression with custom step names
|
|
15516
|
+
*
|
|
15517
|
+
* Unlike trackFunnelStep which uses FunnelStatus enum values,
|
|
15518
|
+
* this method allows any string as the step name for flexible funnel tracking.
|
|
15519
|
+
*
|
|
15520
|
+
* @param funnelName - Name of the funnel (e.g., "checkout", "onboarding")
|
|
15521
|
+
* @param stepName - Custom step name (e.g., "cart_viewed", "payment_entered")
|
|
15522
|
+
* @param stepNumber - Optional numeric position in the funnel
|
|
15523
|
+
* @param attributes - Optional event attributes
|
|
15524
|
+
*/
|
|
15525
|
+
async trackFunnelProgression(funnelName, stepName, stepNumber, attributes) {
|
|
15526
|
+
if (!this.enabled) return;
|
|
15527
|
+
const payload = {
|
|
15528
|
+
type: "funnel",
|
|
15529
|
+
name: `${funnelName}.${stepName}`,
|
|
15530
|
+
funnel: funnelName,
|
|
15531
|
+
step: stepName,
|
|
15532
|
+
stepName,
|
|
15533
|
+
stepNumber,
|
|
15534
|
+
attributes,
|
|
15535
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
15536
|
+
};
|
|
15537
|
+
await this.send(payload);
|
|
15538
|
+
}
|
|
15485
15539
|
/**
|
|
15486
15540
|
* Flush pending requests and clean up
|
|
15487
15541
|
*
|
|
@@ -15542,19 +15596,47 @@ var PostHogSubscriber = class extends EventSubscriber {
|
|
|
15542
15596
|
posthog = null;
|
|
15543
15597
|
config;
|
|
15544
15598
|
initPromise = null;
|
|
15599
|
+
/** True when using browser's window.posthog (different API signature) */
|
|
15600
|
+
isBrowserClient = false;
|
|
15545
15601
|
constructor(config) {
|
|
15546
15602
|
super();
|
|
15547
|
-
if (
|
|
15548
|
-
|
|
15603
|
+
if (config.serverless) {
|
|
15604
|
+
config = {
|
|
15605
|
+
flushAt: 1,
|
|
15606
|
+
flushInterval: 0,
|
|
15607
|
+
requestTimeout: 3e3,
|
|
15608
|
+
...config
|
|
15609
|
+
// User config overrides serverless defaults
|
|
15610
|
+
};
|
|
15611
|
+
}
|
|
15612
|
+
if (!config.apiKey && !config.client && !config.useGlobalClient) {
|
|
15613
|
+
throw new Error(
|
|
15614
|
+
"PostHogSubscriber requires either apiKey, client, or useGlobalClient to be provided"
|
|
15615
|
+
);
|
|
15549
15616
|
}
|
|
15550
15617
|
this.enabled = config.enabled ?? true;
|
|
15551
|
-
this.config =
|
|
15618
|
+
this.config = {
|
|
15619
|
+
filterUndefinedValues: true,
|
|
15620
|
+
...config
|
|
15621
|
+
};
|
|
15552
15622
|
if (this.enabled) {
|
|
15553
15623
|
this.initPromise = this.initialize();
|
|
15554
15624
|
}
|
|
15555
15625
|
}
|
|
15556
15626
|
async initialize() {
|
|
15557
15627
|
try {
|
|
15628
|
+
if (this.config.useGlobalClient) {
|
|
15629
|
+
const globalWindow = typeof globalThis === "undefined" ? void 0 : globalThis;
|
|
15630
|
+
if (globalWindow?.posthog) {
|
|
15631
|
+
this.posthog = globalWindow.posthog;
|
|
15632
|
+
this.isBrowserClient = true;
|
|
15633
|
+
this.setupErrorHandling();
|
|
15634
|
+
return;
|
|
15635
|
+
}
|
|
15636
|
+
throw new Error(
|
|
15637
|
+
"useGlobalClient enabled but window.posthog not found. Ensure PostHog script is loaded before initializing the subscriber."
|
|
15638
|
+
);
|
|
15639
|
+
}
|
|
15558
15640
|
if (this.config.client) {
|
|
15559
15641
|
this.posthog = this.config.client;
|
|
15560
15642
|
this.setupErrorHandling();
|
|
@@ -15601,19 +15683,31 @@ var PostHogSubscriber = class extends EventSubscriber {
|
|
|
15601
15683
|
*/
|
|
15602
15684
|
async sendToDestination(payload) {
|
|
15603
15685
|
await this.ensureInitialized();
|
|
15604
|
-
|
|
15686
|
+
const filteredAttributes = this.config.filterUndefinedValues === false ? payload.attributes : this.filterAttributes(payload.attributes);
|
|
15687
|
+
const properties = { ...filteredAttributes };
|
|
15605
15688
|
if (payload.value !== void 0) {
|
|
15606
|
-
properties =
|
|
15689
|
+
properties.value = payload.value;
|
|
15607
15690
|
}
|
|
15608
|
-
|
|
15609
|
-
|
|
15610
|
-
|
|
15611
|
-
|
|
15612
|
-
|
|
15613
|
-
|
|
15614
|
-
|
|
15691
|
+
if (payload.stepNumber !== void 0) {
|
|
15692
|
+
properties.step_number = payload.stepNumber;
|
|
15693
|
+
}
|
|
15694
|
+
if (payload.stepName !== void 0) {
|
|
15695
|
+
properties.step_name = payload.stepName;
|
|
15696
|
+
}
|
|
15697
|
+
const distinctId = this.extractDistinctId(filteredAttributes);
|
|
15698
|
+
if (this.isBrowserClient) {
|
|
15699
|
+
this.posthog?.capture(payload.name, properties);
|
|
15700
|
+
} else {
|
|
15701
|
+
const capturePayload = {
|
|
15702
|
+
distinctId,
|
|
15703
|
+
event: payload.name,
|
|
15704
|
+
properties
|
|
15705
|
+
};
|
|
15706
|
+
if (filteredAttributes?.groups) {
|
|
15707
|
+
capturePayload.groups = filteredAttributes.groups;
|
|
15708
|
+
}
|
|
15709
|
+
this.posthog?.capture(capturePayload);
|
|
15615
15710
|
}
|
|
15616
|
-
this.posthog?.capture(capturePayload);
|
|
15617
15711
|
}
|
|
15618
15712
|
// Feature Flag Methods
|
|
15619
15713
|
/**
|
|
@@ -15835,6 +15929,15 @@ var PostHogSubscriber = class extends EventSubscriber {
|
|
|
15835
15929
|
*/
|
|
15836
15930
|
handleError(error, payload) {
|
|
15837
15931
|
this.config.onError?.(error);
|
|
15932
|
+
if (this.config.onErrorWithContext) {
|
|
15933
|
+
this.config.onErrorWithContext({
|
|
15934
|
+
error,
|
|
15935
|
+
eventName: payload.name,
|
|
15936
|
+
eventType: payload.type,
|
|
15937
|
+
attributes: payload.attributes,
|
|
15938
|
+
subscriberName: this.name
|
|
15939
|
+
});
|
|
15940
|
+
}
|
|
15838
15941
|
super.handleError(error, payload);
|
|
15839
15942
|
}
|
|
15840
15943
|
};
|