@koryla/node 0.1.0 → 0.1.1
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 +46 -31
- package/dist/index.cjs +24 -2
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +22 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,44 +9,59 @@ Works on Railway, Render, Fly.io, Heroku, AWS EC2/ECS, or any Node host.
|
|
|
9
9
|
npm install @koryla/node
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
## Setup
|
|
12
|
+
## Setup
|
|
13
|
+
|
|
14
|
+
### 1. Add environment variables
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
KORYLA_API_KEY=sk_live_...
|
|
18
|
+
KORYLA_API_URL=https://koryla.com
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### 2. Add the middleware
|
|
22
|
+
|
|
23
|
+
Place it **before** your routes — Koryla rewrites `req.url` internally so Express routes to the correct handler:
|
|
13
24
|
|
|
14
25
|
```ts
|
|
26
|
+
// server.ts
|
|
15
27
|
import express from 'express'
|
|
16
28
|
import { korylaMiddleware } from '@koryla/node'
|
|
17
29
|
|
|
18
30
|
const app = express()
|
|
19
31
|
|
|
20
|
-
// Add Koryla before your routes
|
|
21
32
|
app.use(korylaMiddleware({
|
|
22
33
|
apiKey: process.env.KORYLA_API_KEY!,
|
|
23
|
-
apiUrl: process.env.KORYLA_API_URL!,
|
|
34
|
+
apiUrl: process.env.KORYLA_API_URL!,
|
|
24
35
|
}))
|
|
25
36
|
|
|
26
|
-
//
|
|
37
|
+
// Routes come after — they receive the rewritten req.url
|
|
27
38
|
app.get('/', (req, res) => res.render('home-control'))
|
|
28
39
|
app.get('/variant-b', (req, res) => res.render('home-variant-b'))
|
|
29
|
-
app.get('/pricing', (req, res) => res.render('pricing-control'))
|
|
30
|
-
app.get('/pricing-b', (req, res) => res.render('pricing-variant-b'))
|
|
31
40
|
```
|
|
32
41
|
|
|
33
|
-
###
|
|
42
|
+
### 3. Create your variant pages
|
|
34
43
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
Variant pages must be real routes in the same Express app:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
app/
|
|
48
|
+
views/
|
|
49
|
+
home-control.ejs ← control (base_url: https://acme.com/)
|
|
50
|
+
home-variant-b.ejs ← variant (target_url: https://acme.com/variant-b)
|
|
38
51
|
```
|
|
39
52
|
|
|
40
|
-
|
|
53
|
+
### 4. Create an experiment in Koryla
|
|
41
54
|
|
|
42
|
-
|
|
43
|
-
`req.url` internally and calls `next()`. This means:
|
|
55
|
+
Go to your Koryla dashboard → New experiment:
|
|
44
56
|
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
| Field | Value |
|
|
58
|
+
|-------|-------|
|
|
59
|
+
| Base URL | `https://acme.com/` |
|
|
60
|
+
| Variant A — Control | `https://acme.com/` · 50% |
|
|
61
|
+
| Variant B | `https://acme.com/variant-b` · 50% |
|
|
62
|
+
| Conversion URL | `https://acme.com/thank-you` |
|
|
47
63
|
|
|
48
|
-
|
|
49
|
-
(nginx, Caddy) in front of your app instead.
|
|
64
|
+
Set it to **Active** and you're done.
|
|
50
65
|
|
|
51
66
|
## How it works
|
|
52
67
|
|
|
@@ -56,25 +71,25 @@ User visits /
|
|
|
56
71
|
▼
|
|
57
72
|
Express middleware (in-process, ~1ms overhead)
|
|
58
73
|
│
|
|
59
|
-
├──
|
|
60
|
-
├── reads
|
|
61
|
-
├──
|
|
62
|
-
└──
|
|
74
|
+
├── calls Koryla API once every 60s to get experiment config
|
|
75
|
+
├── reads koryla_sid cookie
|
|
76
|
+
│ ├── cookie present → use existing variant (sticky)
|
|
77
|
+
│ └── no cookie → assign variant by traffic weight
|
|
78
|
+
│
|
|
79
|
+
├── rewrites req.url to /variant-b
|
|
80
|
+
└── sets Set-Cookie header for 30 days
|
|
63
81
|
│
|
|
64
82
|
▼
|
|
65
83
|
Express routes /variant-b to its handler
|
|
66
84
|
Browser sees original URL (/) — no redirect, no flicker
|
|
67
85
|
```
|
|
68
86
|
|
|
69
|
-
##
|
|
87
|
+
## Why this is better than VWO / Optimizely
|
|
70
88
|
|
|
71
|
-
| |
|
|
89
|
+
| | VWO / Optimizely | @koryla/node |
|
|
72
90
|
|--|--|--|
|
|
73
|
-
|
|
|
74
|
-
|
|
|
75
|
-
|
|
|
76
|
-
|
|
|
77
|
-
|
|
|
78
|
-
|
|
79
|
-
Both approaches are server-side — neither has the flicker problem of client-side tools.
|
|
80
|
-
The only difference is where the server runs.
|
|
91
|
+
| When variant is decided | In the browser, after JS loads | On the server, before any HTML |
|
|
92
|
+
| Flicker | Yes (page hides while swapping) | No |
|
|
93
|
+
| Extra JS on page | ~80–150 KB | 0 KB |
|
|
94
|
+
| Blockable by ad blockers | Yes | No |
|
|
95
|
+
| Works without JS | No | Yes |
|
package/dist/index.cjs
CHANGED
|
@@ -20,7 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
-
korylaMiddleware: () => korylaMiddleware
|
|
23
|
+
korylaMiddleware: () => korylaMiddleware,
|
|
24
|
+
trackConversion: () => trackConversion
|
|
24
25
|
});
|
|
25
26
|
module.exports = __toCommonJS(index_exports);
|
|
26
27
|
var import_core = require("@koryla/core");
|
|
@@ -57,7 +58,28 @@ function korylaMiddleware(options) {
|
|
|
57
58
|
}
|
|
58
59
|
};
|
|
59
60
|
}
|
|
61
|
+
async function trackConversion(req, options) {
|
|
62
|
+
const cookieHeader = req.headers["cookie"] ?? "";
|
|
63
|
+
const cookies = Object.fromEntries(
|
|
64
|
+
cookieHeader.split(";").map((c) => c.trim().split("=").map((s) => s.trim())).filter(([k]) => k?.length > 0)
|
|
65
|
+
);
|
|
66
|
+
const sessionId = cookies["ky_sid"];
|
|
67
|
+
if (!sessionId) return;
|
|
68
|
+
for (const [key, value] of Object.entries(cookies)) {
|
|
69
|
+
if (!key.startsWith("ky_") || key === "ky_sid") continue;
|
|
70
|
+
const experimentId = key.slice(3);
|
|
71
|
+
try {
|
|
72
|
+
await fetch(`${options.apiUrl}/api/worker/event`, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${options.apiKey}` },
|
|
75
|
+
body: JSON.stringify({ experiment_id: experimentId, variant_id: value, session_id: sessionId, event_type: "conversion" })
|
|
76
|
+
});
|
|
77
|
+
} catch {
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
60
81
|
// Annotate the CommonJS export names for ESM import in node:
|
|
61
82
|
0 && (module.exports = {
|
|
62
|
-
korylaMiddleware
|
|
83
|
+
korylaMiddleware,
|
|
84
|
+
trackConversion
|
|
63
85
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -51,5 +51,13 @@ interface NodeResponse {
|
|
|
51
51
|
}
|
|
52
52
|
type NextFunction = (err?: unknown) => void;
|
|
53
53
|
declare function korylaMiddleware(options: SplitEngineOptions): (req: NodeRequest, res: NodeResponse, next: NextFunction) => Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Manually fire a conversion event. Use this when the conversion happens
|
|
56
|
+
* via an API call or form POST rather than a page visit.
|
|
57
|
+
*
|
|
58
|
+
* For page-visit conversions, set the conversion URL on the experiment in
|
|
59
|
+
* the Koryla dashboard — the middleware fires the event automatically.
|
|
60
|
+
*/
|
|
61
|
+
declare function trackConversion(req: NodeRequest, options: SplitEngineOptions): Promise<void>;
|
|
54
62
|
|
|
55
|
-
export { type NextFunction, type NodeRequest, type NodeResponse, korylaMiddleware };
|
|
63
|
+
export { type NextFunction, type NodeRequest, type NodeResponse, korylaMiddleware, trackConversion };
|
package/dist/index.d.ts
CHANGED
|
@@ -51,5 +51,13 @@ interface NodeResponse {
|
|
|
51
51
|
}
|
|
52
52
|
type NextFunction = (err?: unknown) => void;
|
|
53
53
|
declare function korylaMiddleware(options: SplitEngineOptions): (req: NodeRequest, res: NodeResponse, next: NextFunction) => Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Manually fire a conversion event. Use this when the conversion happens
|
|
56
|
+
* via an API call or form POST rather than a page visit.
|
|
57
|
+
*
|
|
58
|
+
* For page-visit conversions, set the conversion URL on the experiment in
|
|
59
|
+
* the Koryla dashboard — the middleware fires the event automatically.
|
|
60
|
+
*/
|
|
61
|
+
declare function trackConversion(req: NodeRequest, options: SplitEngineOptions): Promise<void>;
|
|
54
62
|
|
|
55
|
-
export { type NextFunction, type NodeRequest, type NodeResponse, korylaMiddleware };
|
|
63
|
+
export { type NextFunction, type NodeRequest, type NodeResponse, korylaMiddleware, trackConversion };
|
package/dist/index.js
CHANGED
|
@@ -33,6 +33,27 @@ function korylaMiddleware(options) {
|
|
|
33
33
|
}
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
+
async function trackConversion(req, options) {
|
|
37
|
+
const cookieHeader = req.headers["cookie"] ?? "";
|
|
38
|
+
const cookies = Object.fromEntries(
|
|
39
|
+
cookieHeader.split(";").map((c) => c.trim().split("=").map((s) => s.trim())).filter(([k]) => k?.length > 0)
|
|
40
|
+
);
|
|
41
|
+
const sessionId = cookies["ky_sid"];
|
|
42
|
+
if (!sessionId) return;
|
|
43
|
+
for (const [key, value] of Object.entries(cookies)) {
|
|
44
|
+
if (!key.startsWith("ky_") || key === "ky_sid") continue;
|
|
45
|
+
const experimentId = key.slice(3);
|
|
46
|
+
try {
|
|
47
|
+
await fetch(`${options.apiUrl}/api/worker/event`, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${options.apiKey}` },
|
|
50
|
+
body: JSON.stringify({ experiment_id: experimentId, variant_id: value, session_id: sessionId, event_type: "conversion" })
|
|
51
|
+
});
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
36
56
|
export {
|
|
37
|
-
korylaMiddleware
|
|
57
|
+
korylaMiddleware,
|
|
58
|
+
trackConversion
|
|
38
59
|
};
|