Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/v1-remote-analytics-engine.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@cloudflare/vite-plugin": minor
---

Support remote Analytics Engine bindings in Vite plugin v1

Set `remote: true` on an `analytics_engine_datasets` entry in `wrangler.jsonc` to send `writeDataPoint()` calls from local development and preview to the real dataset.
62 changes: 62 additions & 0 deletions packages/remote-bindings/src/analytics-engine-proxy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { newWebSocketRpcSession } from "capnweb";
import { Miniflare } from "miniflare";
import { test } from "vitest";
import remoteBindingsWorkerSource from "worker:remoteBindings/ProxyServerWorker";

test("forwards Analytics Engine writes through the retained remote proxy", async ({
expect,
onTestFinished,
}) => {
const miniflare = new Miniflare({
workers: [
{
config: {
name: "analytics-engine-proxy",
compatibilityDate: "2026-10-23",
manifest: {
mainModule: "index.mjs",
modules: {
"proxy.mjs": {
type: "esm",
contents: remoteBindingsWorkerSource,
},
"index.mjs": {
type: "esm",
contents: `
import proxy from "./proxy.mjs";
const writes = [];
// Like the native binding, this has a custom prototype that capnweb cannot expose directly.
class AnalyticsEngine {
writeDataPoint(event) { writes.push(event); }
}
const dataset = new AnalyticsEngine();
export default {
fetch(request, env, ctx) {
if (new URL(request.url).pathname === "/writes") {
return Response.json(writes);
}
return proxy.fetch(request, { EVENTS: dataset }, ctx);
}
};`,
},
},
},
env: {},
},
},
],
});
onTestFinished(() => miniflare.dispose());
const url = await miniflare.ready;
const remote = newWebSocketRpcSession<{
writeDataPoint(event?: { blobs: string[]; indexes: string[] }): void;
}>(`ws://${url.host}/?MF-Binding=EVENTS`);
try {
const event = { blobs: ["vite-v1"], indexes: ["retained-proxy"] };
await remote.writeDataPoint(event);
const response = await miniflare.dispatchFetch(new URL("/writes", url));
expect(await response.json()).toEqual([event]);
} finally {
remote[Symbol.dispose]();
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ class BindingNotFoundError extends Error {
* - SendEmail bindings need to take EmailMessage as their first parameter,
* which is not serialisable. As such, we reconstruct it before sending it
* on to the binding. See packages/miniflare/src/workers/email/email.worker.ts
* - Hyperdrive and Analytics Engine bindings are native objects that capnweb
* can't expose directly, so we wrap their methods in a plain object.
* - Dispatch Namespace bindings have a synchronous .get() method. Since we
* can't emulate that over an async boundary, we mock it locally and _actually_
* perform the .get() remotely at the first appropriate async point. See
Expand Down Expand Up @@ -61,6 +63,15 @@ function getExposedJSRPCBinding(request: Request, env: Env) {
};
}

if (targetBinding.constructor.name === "AnalyticsEngine") {
const dataset = targetBinding as AnalyticsEngineDataset;
return {
writeDataPoint(event?: AnalyticsEngineDataPoint) {
dataset.writeDataPoint(event);
},
};
}

if (targetBinding.constructor.name === "SendEmail") {
return {
async send(e: SendEmailInput) {
Expand Down
Loading