site: a partner's website and key links go through usemagpie.ai/go, which counts the click; fetches of the partner list are counted too
The app's own partner counts only come from users who leave the stats on, so a sponsor's reach and clicks are now also counted where the server sees them. /api/partners hands out each partner's website and keysUrl (and a region's) as usemagpie.ai/go/<id>/site|keys[/<region>]; /go sends a "magpie partner go" event to PostHog (partner, link, region, country) and 302s to partners.js's own URL. One in ten list fetches sends "magpie partners fetch" with weight 10 while someone is listed. Events carry a random id each, no install id or address. Ended entries leave the list but /go still resolves them, since added providers keep the links. Privacy text updated in README (en, zh), docs/reference.md and the site docs (en, zh, ja). Run: node --test site/*.test.mjs (27 pass); a scratch copy with a fake partner served rewritten links, 302'd /go to the real URLs and posted both events; PostHog's /i/v0/e/ answered 200 to the payload shape.
This commit is contained in:
1 parent
4eb90b69f4
commit
34f2078db8
9 files changed
+205
-12
No files matched your search
@@ -283,7 +283,7 @@ Find them under Plugins › Discover › Gateway middleware. See [Gateway middle
|
||||
|
||||
## Privacy
|
||||
|
||||
Your prompts, replies, keys and accounts go only to the providers you use. Once a day a released magpie tells us it is in use: a random id, its version and system, and which agents, providers and models it is used with, by magpie's own ids (a provider you added yourself is only `custom`), and for each partner listed first in the add sheet, how many times a day it was shown, opened and added (counts only). No names, URLs, accounts, keys, prompts or usage. Turn part or all of it off in **Settings → Privacy**, or with `DO_NOT_TRACK=1`. [What is sent, exactly](docs/reference.md#counting-users).
|
||||
Your prompts, replies, keys and accounts go only to the providers you use. Once a day a released magpie tells us it is in use: a random id, its version and system, and which agents, providers and models it is used with, by magpie's own ids (a provider you added yourself is only `custom`), and for each partner listed first in the add sheet, how many times a day it was shown, opened and added (counts only). No names, URLs, accounts, keys, prompts or usage. Turn part or all of it off in **Settings → Privacy**, or with `DO_NOT_TRACK=1`. A partner's website and key links go through `usemagpie.ai/go/…`, which counts the click, and usemagpie.ai counts fetches of the partner list; the server sees these whatever the settings, and keeps no id or address. [What is sent, exactly](docs/reference.md#counting-users).
|
||||
|
||||
## Community
|
||||
|
||||
|
||||
+1
-1
@@ -283,7 +283,7 @@ magpie plugin options model-map '{"mapping": {"fast": "deepseek/deepseek-chat"}}
|
||||
|
||||
## 隐私
|
||||
|
||||
你的提示词、回复、密钥和账号只发给你用的供应商。正式版 magpie 每天告诉我们一次它在用:一个随机 ID、版本和系统,以及用了哪些 agent、供应商和模型,只用 magpie 自己的 ID(你自己添加的供应商只记作 `custom`),以及添加供应商页里排在最前的合作伙伴每天被展示、打开和添加的次数(只有次数)。不发送名称、URL、账号、密钥、提示词和用量。可以在 **设置 → 隐私** 里关掉一部分或全部,也可以设 `DO_NOT_TRACK=1`。[具体发送哪些内容](docs/reference.md#counting-users)。
|
||||
你的提示词、回复、密钥和账号只发给你用的供应商。正式版 magpie 每天告诉我们一次它在用:一个随机 ID、版本和系统,以及用了哪些 agent、供应商和模型,只用 magpie 自己的 ID(你自己添加的供应商只记作 `custom`),以及添加供应商页里排在最前的合作伙伴每天被展示、打开和添加的次数(只有次数)。不发送名称、URL、账号、密钥、提示词和用量。可以在 **设置 → 隐私** 里关掉一部分或全部,也可以设 `DO_NOT_TRACK=1`。合作伙伴的官网和获取 Key 链接经 `usemagpie.ai/go/…` 跳转并计一次点击,usemagpie.ai 也统计合作伙伴列表的拉取次数;这是服务器本身看到的,不受设置影响,不记录 ID 和 IP。[具体发送哪些内容](docs/reference.md#counting-users)。
|
||||
|
||||
## 社区
|
||||
|
||||
|
||||
@@ -1959,6 +1959,17 @@ Settings → Privacy → Count me as a user, or with `DO_NOT_TRACK=1` or
|
||||
`MAGPIE_NO_STATS=1`. Builds from source never send it. The code is
|
||||
[internal/stats](../internal/stats/stats.go).
|
||||
|
||||
Partners are also counted where usemagpie.ai sees them, whatever these
|
||||
settings say, as any website sees its visits. The partner list
|
||||
(`/api/partners`) gives each partner's website and key page as a
|
||||
`https://usemagpie.ai/go/<id>/site|keys[/<region>]` link, which sends a
|
||||
`magpie partner go` event (the partner, which link, the region, and the
|
||||
country Cloudflare gives) before sending the browser on; and one in ten
|
||||
fetches of the list sends a `magpie partners fetch` event with weight 10
|
||||
and the country, while a partner is listed. Each event has an id of its
|
||||
own: no install id, address or account goes with it. The code is
|
||||
[site/worker.js](../site/worker.js).
|
||||
|
||||
## Community
|
||||
|
||||
Questions, setups worth sharing, ideas, bugs: come talk to us and other
|
||||
|
||||
+8
-2
@@ -2,8 +2,8 @@
|
||||
// magpie's "Add provider" sheet, under a heading that says they sponsor
|
||||
// magpie. The app fetches this list from /api/partners every six hours and
|
||||
// keeps it (internal/provider/partners.go), so a change here reaches users
|
||||
// with `npx wrangler deploy`, no release. At most 12 are listed, in this
|
||||
// order.
|
||||
// with `npx wrangler deploy`, no release. At most 12 whose term hasn't
|
||||
// ended are listed, in this order.
|
||||
//
|
||||
// An entry is a preset (internal/provider/presets.go) and a few fields more:
|
||||
//
|
||||
@@ -26,4 +26,10 @@
|
||||
// from, until RFC 3339 times bounding when it is listed (the paid term)
|
||||
//
|
||||
// Every URL is https. An entry that breaks a rule is left out by the app.
|
||||
//
|
||||
// The app gets keysUrl and website (a region's too) as usemagpie.ai/go/…
|
||||
// links, which count the click and send the browser on (worker.js). The
|
||||
// providers users add keep those links, so an entry stays here after its
|
||||
// term ends: /api/partners leaves it out, /go still finds it. Remove one
|
||||
// only when nobody can still be using it.
|
||||
export const PARTNERS = [];
|
||||
@@ -266,7 +266,7 @@ magpie claude group/opus-anywhere <span class="c"># これを使う</span></p
|
||||
<p>インストーラと <code>magpie update</code> にプロキシを渡します:<code>curl -fsSL https://usemagpie.ai/install.sh | sh -s -- --proxy http://127.0.0.1:7890</code>、または <code>magpie update --proxy socks5://127.0.0.1:1080</code>(http・https・socks5・socks5h に対応。<code>HTTPS_PROXY</code> と <code>ALL_PROXY</code> も使われます)。<code>--mirror <prefix></code> を付けると、指定した GitHub ダウンロードミラーから <code><prefix>https://github.com/…</code> としてリリースファイルを取得します。<code>magpie update mirror <prefix></code> でアプリ自身の更新を含め毎回使い、<code>magpie update mirror off</code> で解除します。指定しない限りミラーは使われず、ファイルの SHA-256 はミラーではなく常に usemagpie.ai から取得するので、ミラーが書き換えたファイルはインストールされません。アプリ自身の更新は <span class="ui">Settings</span> → <span class="ui">Proxy</span> を通ります。</p>
|
||||
|
||||
<h3 id="privacy">magpie は何を送信しますか?</h3>
|
||||
<p>プロンプト、返答、キー、アカウントは、使っているプロバイダーにだけ送られます。間に magpie 自身のサーバーはありません。リリース版の magpie は 1 日 1 回、使われていることを知らせます。何人が使っていて、何を優先して面倒を見るべきかを知るためです。送るのは、このコンピュータで作ったランダムな ID、magpie のバージョンとシステム、そして使っているエージェント、プロバイダー、モデルを magpie 自身の ID(<code>claude</code>、<code>deepseek</code>、<code>deepseek/deepseek-v4</code>)で、それぞれの数とともに。自分で追加したプロバイダーとそのモデルは <code>custom</code> とだけ記録します。プロバイダー追加シートの先頭に載るパートナーについては、1 日に表示された回数、開かれた回数、追加された回数も送ります(回数のみ)。名前、Base URL、アカウント、キー、プロンプト、使用量は送りません。<span class="ui">設定</span> → <span class="ui">プライバシー</span> → <span class="ui">使っているエージェント、プロバイダー、モデルを共有</span> でエージェント、プロバイダー、モデルとパートナーの回数を外せます。<span class="ui">ユーザー数に含める</span>、<code>DO_NOT_TRACK=1</code>、<code>MAGPIE_NO_STATS=1</code> のどれかですべてオフになります。ソースからのビルドは送信しません。詳しくは<a href="https://github.com/yetone/magpie/blob/main/docs/reference.md#counting-users">リファレンス</a>を。</p>
|
||||
<p>プロンプト、返答、キー、アカウントは、使っているプロバイダーにだけ送られます。間に magpie 自身のサーバーはありません。リリース版の magpie は 1 日 1 回、使われていることを知らせます。何人が使っていて、何を優先して面倒を見るべきかを知るためです。送るのは、このコンピュータで作ったランダムな ID、magpie のバージョンとシステム、そして使っているエージェント、プロバイダー、モデルを magpie 自身の ID(<code>claude</code>、<code>deepseek</code>、<code>deepseek/deepseek-v4</code>)で、それぞれの数とともに。自分で追加したプロバイダーとそのモデルは <code>custom</code> とだけ記録します。プロバイダー追加シートの先頭に載るパートナーについては、1 日に表示された回数、開かれた回数、追加された回数も送ります(回数のみ)。名前、Base URL、アカウント、キー、プロンプト、使用量は送りません。<span class="ui">設定</span> → <span class="ui">プライバシー</span> → <span class="ui">使っているエージェント、プロバイダー、モデルを共有</span> でエージェント、プロバイダー、モデルとパートナーの回数を外せます。<span class="ui">ユーザー数に含める</span>、<code>DO_NOT_TRACK=1</code>、<code>MAGPIE_NO_STATS=1</code> のどれかですべてオフになります。ソースからのビルドは送信しません。パートナーの公式サイトと「キーを取得」のリンクは <code>usemagpie.ai/go/…</code> を経由し、移動の前にクリックを 1 回数えます(どのパートナーか、どのリンクか、国)。usemagpie.ai はパートナー一覧が取得された回数も数えます。これはサーバー自身が見るもので、どの Web サイトのアクセス集計とも同じく、上の設定には左右されません。ID、IP アドレス、アカウントは記録しません。詳しくは<a href="https://github.com/yetone/magpie/blob/main/docs/reference.md#counting-users">リファレンス</a>を。</p>
|
||||
|
||||
<h3 id="proxy-loopback">エージェントが 502 を返す、または接続できず、magpie の Routing タブに何も出ない?</h3>
|
||||
<p>プロキシアプリ(Surge、Clash、mihomo など。システムプロキシ、TUN、拡張モードのいずれかが有効)が、エージェントから <code>http://127.0.0.1:3425</code> へのリクエストを横取りして別の場所へ送っているため、magpie に届いていません。除外リスト(<code>skip-proxy</code>、バイパス)に 127.0.0.1 を書くだけでは足りないことがあります。Codex など一部のエージェントは、それでもプロキシを通します。ローカルアドレスを直結にするルールを、他のルールより前に追加してください:</p>
|
||||
|
||||
@@ -266,7 +266,7 @@ magpie claude group/opus-anywhere <span class="c"># use it</span></pre>
|
||||
<p>Give the installer and <code>magpie update</code> a proxy: <code>curl -fsSL https://usemagpie.ai/install.sh | sh -s -- --proxy http://127.0.0.1:7890</code>, or <code>magpie update --proxy socks5://127.0.0.1:1080</code> (http, https, socks5 and socks5h; <code>HTTPS_PROXY</code> and <code>ALL_PROXY</code> are honoured too). <code>--mirror <prefix></code> fetches the release file from a GitHub download mirror you choose, as <code><prefix>https://github.com/…</code>; <code>magpie update mirror <prefix></code> keeps it for every update, the app’s too, and <code>magpie update mirror off</code> removes it. No mirror is used unless you give one, and the file’s SHA-256 still comes from usemagpie.ai, never from the mirror: a file the mirror changed is refused. The app’s own updates go through <span class="ui">Settings</span> → <span class="ui">Proxy</span>.</p>
|
||||
|
||||
<h3 id="privacy">What does magpie send about me?</h3>
|
||||
<p>Your prompts, replies, keys and accounts go only to the providers you use; magpie has no server of its own in the path. Once a day a released magpie tells us it is in use, so we know how many people use it and what to look after first: a random id made up on your computer, magpie’s version and your system, and which agents, providers and models it is used with, by magpie’s own ids (<code>claude</code>, <code>deepseek</code>, <code>deepseek/deepseek-v4</code>) with how many of each. A provider you added yourself, and a model on it, is only <code>custom</code>. For each partner listed first in the add sheet, how many times a day it was shown, opened and added from goes with them, as counts only. No names, base URLs, accounts, keys, prompts or usage are sent. <span class="ui">Settings</span> → <span class="ui">Privacy</span> → <span class="ui">Share the agents, providers and models I use</span> leaves out the agents, providers and models, and the partner counts; <span class="ui">Count me as a user</span>, <code>DO_NOT_TRACK=1</code> or <code>MAGPIE_NO_STATS=1</code> turns it all off. Builds from source never send it. The details are in the <a href="https://github.com/yetone/magpie/blob/main/docs/reference.md#counting-users">reference</a>.</p>
|
||||
<p>Your prompts, replies, keys and accounts go only to the providers you use; magpie has no server of its own in the path. Once a day a released magpie tells us it is in use, so we know how many people use it and what to look after first: a random id made up on your computer, magpie’s version and your system, and which agents, providers and models it is used with, by magpie’s own ids (<code>claude</code>, <code>deepseek</code>, <code>deepseek/deepseek-v4</code>) with how many of each. A provider you added yourself, and a model on it, is only <code>custom</code>. For each partner listed first in the add sheet, how many times a day it was shown, opened and added from goes with them, as counts only. No names, base URLs, accounts, keys, prompts or usage are sent. <span class="ui">Settings</span> → <span class="ui">Privacy</span> → <span class="ui">Share the agents, providers and models I use</span> leaves out the agents, providers and models, and the partner counts; <span class="ui">Count me as a user</span>, <code>DO_NOT_TRACK=1</code> or <code>MAGPIE_NO_STATS=1</code> turns it all off. Builds from source never send it. A partner's website and key-page links go through <code>usemagpie.ai/go/…</code>, which counts the click (the partner, which link, and a country) before sending you on, and usemagpie.ai counts how many times the partner list is fetched. The server sees these whatever the settings say, as any website sees its visits; no id, address or account is kept with them. The details are in the <a href="https://github.com/yetone/magpie/blob/main/docs/reference.md#counting-users">reference</a>.</p>
|
||||
|
||||
<h3 id="proxy-loopback">The agent gets 502 or can’t connect, and nothing shows in magpie’s Routing tab?</h3>
|
||||
<p>A proxy app (Surge, Clash, mihomo and the like, with system proxy, TUN or enhanced mode on) has taken the agent’s request to <code>http://127.0.0.1:3425</code> and sent it somewhere else, so it never reaches magpie. The bypass list (<code>skip-proxy</code>, “bypass”) isn’t always enough: some agents, Codex among them, send it through the proxy anyway. Add rules that send the local addresses direct, ahead of your other rules:</p>
|
||||
|
||||
@@ -266,7 +266,7 @@ magpie claude group/opus-anywhere <span class="c"># 使用它</span></pre>
|
||||
<p>给安装脚本和 <code>magpie update</code> 加代理:<code>curl -fsSL https://usemagpie.ai/install.sh | sh -s -- --proxy http://127.0.0.1:7890</code>,或 <code>magpie update --proxy socks5://127.0.0.1:1080</code>(支持 http、https、socks5、socks5h;环境变量 <code>HTTPS_PROXY</code>、<code>ALL_PROXY</code> 同样生效)。<code>--mirror <前缀></code> 从你指定的 GitHub 下载镜像取发布文件,即 <code><前缀>https://github.com/…</code>;<code>magpie update mirror <前缀></code> 让之后每次更新(包括应用自己的更新)都用它,<code>magpie update mirror off</code> 取消。不指定就不用任何镜像;文件的 SHA-256 始终来自 usemagpie.ai,而不是镜像,镜像改过的文件会被拒绝安装。应用自己的更新走 <span class="ui">设置</span> → <span class="ui">代理</span>。</p>
|
||||
|
||||
<h3 id="privacy">magpie 会发送我的哪些信息?</h3>
|
||||
<p>你的提示词、回复、密钥和账号只发给你用的供应商,中间没有 magpie 自己的服务器。正式发布的 magpie 每天告诉我们一次它在用,好让我们知道有多少人在用、该先照顾哪些:本机随机生成的 ID、magpie 版本和系统,以及用了哪些 agent、供应商和模型,只用 magpie 自己的 ID(<code>claude</code>、<code>deepseek</code>、<code>deepseek/deepseek-v4</code>)和各自的数量。你自己添加的供应商及其上的模型只记作 <code>custom</code>。添加供应商页里排在最前的合作伙伴,每天被展示、打开和添加的次数也一并发送,只有次数。不发送名称、Base URL、账号、密钥、提示词和用量。<span class="ui">设置</span> → <span class="ui">隐私</span> → <span class="ui">分享我用的 agent、provider 和模型</span> 可以去掉 agent、供应商、模型和合作伙伴次数这部分;<span class="ui">计入用户数</span>、<code>DO_NOT_TRACK=1</code> 或 <code>MAGPIE_NO_STATS=1</code> 全部关闭。从源码构建的版本从不发送。详见<a href="https://github.com/yetone/magpie/blob/main/docs/reference.md#counting-users">参考文档</a>。</p>
|
||||
<p>你的提示词、回复、密钥和账号只发给你用的供应商,中间没有 magpie 自己的服务器。正式发布的 magpie 每天告诉我们一次它在用,好让我们知道有多少人在用、该先照顾哪些:本机随机生成的 ID、magpie 版本和系统,以及用了哪些 agent、供应商和模型,只用 magpie 自己的 ID(<code>claude</code>、<code>deepseek</code>、<code>deepseek/deepseek-v4</code>)和各自的数量。你自己添加的供应商及其上的模型只记作 <code>custom</code>。添加供应商页里排在最前的合作伙伴,每天被展示、打开和添加的次数也一并发送,只有次数。不发送名称、Base URL、账号、密钥、提示词和用量。<span class="ui">设置</span> → <span class="ui">隐私</span> → <span class="ui">分享我用的 agent、provider 和模型</span> 可以去掉 agent、供应商、模型和合作伙伴次数这部分;<span class="ui">计入用户数</span>、<code>DO_NOT_TRACK=1</code> 或 <code>MAGPIE_NO_STATS=1</code> 全部关闭。从源码构建的版本从不发送。合作伙伴的官网和「获取 Key」链接经 <code>usemagpie.ai/go/…</code> 跳转,跳转前记一次点击(哪个合作伙伴、哪个链接、哪个国家),usemagpie.ai 也会统计合作伙伴列表被拉取的次数。这部分是服务器本身看到的,和任何网站统计访问一样,不受上面的设置影响;不记录 ID、IP 和账号。详见<a href="https://github.com/yetone/magpie/blob/main/docs/reference.md#counting-users">参考文档</a>。</p>
|
||||
|
||||
<h3 id="proxy-loopback">Agent 报 502 或连不上,magpie 的路由标签页里却什么都没有?</h3>
|
||||
<p>代理软件(Surge、Clash、mihomo 等,开了系统代理、TUN 或增强模式)把 Agent 发往 <code>http://127.0.0.1:3425</code> 的请求接走、转去了别处,请求根本没到 magpie。只在跳过列表(<code>skip-proxy</code>、「绕过」)里写 127.0.0.1 不一定够:有些 Agent(比如 Codex)照样会走代理。在代理软件里加两条本地地址直连的规则,放在其他规则前面:</p>
|
||||
|
||||
+78
-2
@@ -8,7 +8,10 @@
|
||||
// Both take ?lang=zh for the notes in Chinese, where a
|
||||
// release has them (below its <!-- lang:zh --> marker);
|
||||
// any other lang, or none, is the English alone.
|
||||
// /api/partners {partners: [...]}: the add sheet's Partners (partners.js)
|
||||
// /api/partners {partners: [...]}: the add sheet's Partners (partners.js),
|
||||
// their website and key links through /go
|
||||
// /go/<id>/keys[/<region>], /go/<id>/site[/<region>]
|
||||
// a partner's key page or website, counted
|
||||
// /download the Apple Silicon dmg
|
||||
// /download/mac-arm64 the same; /download/mac-intel the Intel dmg
|
||||
// /download/windows the Windows app (x64); /download/windows-arm64
|
||||
@@ -59,7 +62,20 @@ export default {
|
||||
return json({ ...got.rel, notes: inLang(got.rel.notes, lang) }, 200, cacheFor(got));
|
||||
}
|
||||
if (url.pathname === "/api/partners") {
|
||||
return json({ partners: PARTNERS }, 200, { "Cache-Control": "public, max-age=600" });
|
||||
const now = Date.now();
|
||||
if (PARTNERS.some((p) => live(p, now)) && Math.random() < 1 / FETCH_SAMPLE)
|
||||
ctx.waitUntil(capture("magpie partners fetch", { weight: FETCH_SAMPLE, country: country(req) }));
|
||||
return json({ partners: served(PARTNERS, now) }, 200, { "Cache-Control": "public, max-age=600" });
|
||||
}
|
||||
if (url.pathname.startsWith("/go/")) {
|
||||
let id, what, region, to;
|
||||
try {
|
||||
[, , id, what, region] = url.pathname.split("/").map(decodeURIComponent);
|
||||
to = goTarget(PARTNERS, id, what, region);
|
||||
} catch {}
|
||||
if (!to) return new Response("not found\n", { status: 404 });
|
||||
if (req.method === "GET") ctx.waitUntil(capture("magpie partner go", { id, what, region: region || "", country: country(req) }));
|
||||
return new Response(null, { status: 302, headers: { Location: to, "Cache-Control": "no-store" } });
|
||||
}
|
||||
if (url.pathname === "/api/notes") {
|
||||
const got = await releases(ctx, env);
|
||||
@@ -117,6 +133,66 @@ export default {
|
||||
},
|
||||
};
|
||||
|
||||
// A partner's reach and clicks are counted here, not in the app: the app's
|
||||
// own counts (provider.CountPartner) go only from users who let the stats
|
||||
// go, and these are what the server sees anyway. /api/partners hands out
|
||||
// each partner's website and key page as a /go link, which counts the
|
||||
// click (event "magpie partner go") and sends the browser on. Each app
|
||||
// fetches the list every six hours, so the fetches tell how many apps a
|
||||
// partner can be seen in: one in FETCH_SAMPLE is sent, as "magpie partners
|
||||
// fetch" with weight FETCH_SAMPLE (sum it), and only while someone is
|
||||
// listed. No id, address or account goes: a country, at most.
|
||||
const FETCH_SAMPLE = 10;
|
||||
const GO = "https://usemagpie.ai/go/";
|
||||
const POSTHOG = "https://us.i.posthog.com/i/v0/e/";
|
||||
// the Magpie project's PostHog key, as the app's (internal/stats): it can
|
||||
// send events and read nothing
|
||||
const POSTHOG_KEY = "phc_nyDyYyrUTRGvNq5U7FBesCyM2j4GdKeqGp8ko7DBerTC";
|
||||
|
||||
// live reports whether a partner's term includes now.
|
||||
function live(p, now) {
|
||||
return (!p.from || Date.parse(p.from) <= now) && (!p.until || now < Date.parse(p.until));
|
||||
}
|
||||
|
||||
// served is the list the app gets: those whose term hasn't ended (an
|
||||
// ended one stays in partners.js, so the /go links of providers added from
|
||||
// it keep working), with their website and key page as /go links.
|
||||
export function served(list, now) {
|
||||
const go = (p, what, r) => GO + [p.id, what, r && r.id].filter(Boolean).map(encodeURIComponent).join("/");
|
||||
const linked = (o, p, r) => ({ ...o, ...(o.website && { website: go(p, "site", r) }), ...(o.keysUrl && { keysUrl: go(p, "keys", r) }) });
|
||||
return list
|
||||
.filter((p) => !p.until || now < Date.parse(p.until))
|
||||
.map((p) => ({ ...linked(p, p), ...(p.regions && { regions: p.regions.map((r) => linked(r, p, r)) }) }));
|
||||
}
|
||||
|
||||
// goTarget is where a /go link sends the browser: the partner's own key
|
||||
// page or website, the region's where it has one. Only partners.js's own
|
||||
// addresses, never one the link names.
|
||||
export function goTarget(list, id, what, region) {
|
||||
const p = list.find((x) => x.id === id);
|
||||
const field = what === "keys" ? "keysUrl" : what === "site" ? "website" : "";
|
||||
if (!p || !field) return "";
|
||||
if (!region) return p[field] || "";
|
||||
const r = (p.regions || []).find((x) => x.id === region);
|
||||
return r ? r[field] || p[field] || "" : "";
|
||||
}
|
||||
|
||||
function country(req) {
|
||||
return (req.cf && req.cf.country) || "";
|
||||
}
|
||||
|
||||
// capture sends one event to PostHog, under an id of its own: a count, not
|
||||
// a person.
|
||||
async function capture(event, props) {
|
||||
try {
|
||||
await fetch(POSTHOG, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ api_key: POSTHOG_KEY, event, distinct_id: crypto.randomUUID(), properties: { ...props, $process_person_profile: false } }),
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// preferred is the home page's language for this browser: the one picked on
|
||||
// the page (the lang cookie), else the first of its Accept-Language that the
|
||||
// site has, else English.
|
||||
|
||||
+103
-3
@@ -2,7 +2,7 @@
|
||||
// (freecss on Discord). GitHub and the edge cache are stood in for.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import worker from "./worker.js";
|
||||
import worker, { served, goTarget } from "./worker.js";
|
||||
|
||||
const EN = "### Features\n\n- One thing (#1)\n\n### Install\n\nDownload it.";
|
||||
const ZH = "### 新功能\n\n- 一件事 (#1)";
|
||||
@@ -52,8 +52,14 @@ const FEED = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
</ul></content>
|
||||
</entry>
|
||||
</feed>`;
|
||||
// what the worker sent PostHog
|
||||
const captured = [];
|
||||
globalThis.fetch = async (u, init = {}) => {
|
||||
u = String(u);
|
||||
if (u === "https://us.i.posthog.com/i/v0/e/") {
|
||||
captured.push(JSON.parse(init.body));
|
||||
return new Response("{}");
|
||||
}
|
||||
if (u.startsWith("https://api.github.com/")) {
|
||||
seen.push(new Headers(init.headers));
|
||||
if (apiDown) return new Response('{"message":"API rate limit exceeded"}', { status: 403 });
|
||||
@@ -279,8 +285,9 @@ test("partners", async () => {
|
||||
const res = await ask("/api/partners");
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(res.headers.get("Cache-Control"), "public, max-age=600");
|
||||
assert.deepEqual(await res.json(), { partners: PARTNERS });
|
||||
assert(PARTNERS.length <= 12);
|
||||
const { partners } = await res.json();
|
||||
assert.deepEqual(partners, served(PARTNERS, Date.now()));
|
||||
assert(partners.length <= 12);
|
||||
const ids = new Set();
|
||||
for (const p of PARTNERS) {
|
||||
assert.match(p.id, /^[a-z0-9][a-z0-9-]{1,40}$/, p.id);
|
||||
@@ -293,3 +300,96 @@ test("partners", async () => {
|
||||
for (const t of [p.from, p.until].filter(Boolean)) assert(!isNaN(Date.parse(t)), p.id + ": " + t);
|
||||
}
|
||||
});
|
||||
|
||||
// The website and key links the app gets go through /go, which counts the
|
||||
// click and sends the browser on; one whose term ended leaves the list but
|
||||
// its links, kept in the providers added from it, still go.
|
||||
const LIST = [
|
||||
{ id: "acme", name: "Acme", chat: "https://api.acme.example/v1", website: "https://acme.example/", keysUrl: "https://acme.example/keys", iconUrl: "https://acme.example/i.png",
|
||||
regions: [{ id: "cn", name: "China", chat: "https://cn.acme.example/v1", keysUrl: "https://cn.acme.example/keys" }, { id: "us east", name: "US", chat: "https://us.acme.example/v1" }] },
|
||||
{ id: "plain", name: "Plain", chat: "https://api.plain.example/v1" },
|
||||
{ id: "gone", name: "Gone", chat: "https://api.gone.example/v1", keysUrl: "https://gone.example/keys", until: "2026-01-01T00:00:00Z" },
|
||||
];
|
||||
const NOW = Date.parse("2026-10-10T00:00:00Z");
|
||||
|
||||
test("partners: links go through /go", () => {
|
||||
const out = served(LIST, NOW);
|
||||
assert.deepEqual(out.map((p) => p.id), ["acme", "plain"]);
|
||||
const [acme, plain] = out;
|
||||
assert.equal(acme.website, "https://usemagpie.ai/go/acme/site");
|
||||
assert.equal(acme.keysUrl, "https://usemagpie.ai/go/acme/keys");
|
||||
assert.equal(acme.iconUrl, LIST[0].iconUrl);
|
||||
assert.equal(acme.chat, LIST[0].chat);
|
||||
assert.equal(acme.regions[0].keysUrl, "https://usemagpie.ai/go/acme/keys/cn");
|
||||
assert.equal(acme.regions[0].website, undefined);
|
||||
assert.equal(acme.regions[1].keysUrl, undefined);
|
||||
assert.deepEqual(plain, LIST[1]);
|
||||
// partners.js itself is left as it is
|
||||
assert.equal(LIST[0].keysUrl, "https://acme.example/keys");
|
||||
assert.equal(served([{ ...LIST[0], until: "2026-10-10T00:00:01Z" }], NOW).length, 1);
|
||||
});
|
||||
|
||||
test("partners: /go sends the browser on", () => {
|
||||
assert.equal(goTarget(LIST, "acme", "keys"), "https://acme.example/keys");
|
||||
assert.equal(goTarget(LIST, "acme", "site"), "https://acme.example/");
|
||||
assert.equal(goTarget(LIST, "acme", "keys", "cn"), "https://cn.acme.example/keys");
|
||||
// a region without its own page has the partner's
|
||||
assert.equal(goTarget(LIST, "acme", "keys", "us east"), "https://acme.example/keys");
|
||||
assert.equal(goTarget(LIST, "acme", "site", "cn"), "https://acme.example/");
|
||||
// ended, still goes
|
||||
assert.equal(goTarget(LIST, "gone", "keys"), "https://gone.example/keys");
|
||||
for (const [id, what, region] of [["nobody", "keys"], ["acme", "chat"], ["acme", "keys", "mars"], ["plain", "keys"], ["acme", "constructor"]])
|
||||
assert.equal(goTarget(LIST, id, what, region), "", [id, what, region].join(" "));
|
||||
});
|
||||
|
||||
test("partners: /go counts the click", async () => {
|
||||
const { PARTNERS } = await import("./partners.js");
|
||||
const p = PARTNERS.find((x) => x.keysUrl || x.website);
|
||||
if (!p) {
|
||||
// nobody listed: every /go is not found, and nothing is sent
|
||||
captured.length = 0;
|
||||
for (const path of ["/go/acme/keys", "/go/%E0/keys", "/go/"]) assert.equal((await ask(path)).status, 404, path);
|
||||
assert.deepEqual(captured, []);
|
||||
return;
|
||||
}
|
||||
const what = p.keysUrl ? "keys" : "site";
|
||||
captured.length = 0;
|
||||
const res = await worker.fetch(new Request("https://usemagpie.ai/go/" + p.id + "/" + what), {}, ctx);
|
||||
assert.equal(res.status, 302);
|
||||
assert.equal(res.headers.get("Location"), p.keysUrl || p.website);
|
||||
await Promise.resolve();
|
||||
assert.equal(captured.length, 1);
|
||||
assert.equal(captured[0].event, "magpie partner go");
|
||||
assert.equal(captured[0].properties.id, p.id);
|
||||
assert.equal(captured[0].properties.what, what);
|
||||
assert.equal(captured[0].properties.$process_person_profile, false);
|
||||
// a HEAD goes on but isn't counted
|
||||
captured.length = 0;
|
||||
const head = await worker.fetch(new Request("https://usemagpie.ai/go/" + p.id + "/" + what, { method: "HEAD" }), {}, ctx);
|
||||
assert.equal(head.status, 302);
|
||||
assert.deepEqual(captured, []);
|
||||
assert.equal((await ask("/go/nobody-at-all/keys")).status, 404);
|
||||
assert.equal((await ask("/go/%E0/keys")).status, 404);
|
||||
});
|
||||
|
||||
test("partners: the list's fetches are counted, one in ten", async () => {
|
||||
const { PARTNERS } = await import("./partners.js");
|
||||
const anyLive = PARTNERS.some((p) => (!p.from || Date.parse(p.from) <= Date.now()) && (!p.until || Date.now() < Date.parse(p.until)));
|
||||
const random = Math.random;
|
||||
try {
|
||||
for (const [r, sent] of [[0.05, anyLive], [0.5, false]]) {
|
||||
Math.random = () => r;
|
||||
captured.length = 0;
|
||||
const res = await ask("/api/partners");
|
||||
assert.equal(res.status, 200);
|
||||
await new Promise((ok) => setTimeout(ok, 0));
|
||||
assert.equal(captured.length, sent ? 1 : 0, "random " + r);
|
||||
if (sent) {
|
||||
assert.equal(captured[0].event, "magpie partners fetch");
|
||||
assert.equal(captured[0].properties.weight, 10);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Math.random = random;
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user