Iscriviti per ricevere notifiche di nuovi post:

Mejoramos las pruebas de Workers con Vitest y workerd

15/03/2024

Lettura di 15 min

Hoy nos complace anunciar la integración de Workers Vitest, que te permitirá escribir pruebas unitarias y de integración a través del popular marco de pruebas Vitest, que se ejecutan directamente en nuestro entorno de ejecución, workerd.

Esta integración te ofrece la posibilidad de probar todo lo relacionado con tu Worker.

Por primera vez, podrás escribir pruebas unitarias que se ejecuten en el mismo entorno de ejecución en el que se ejecuta Cloudflare Workers en producción, lo que proporciona mayor confianza en que el comportamiento de tu Worker en las pruebas será el mismo que cuando se implemente en producción. Para las pruebas de integración, podrás escribir pruebas para Workers que se activan mediante Cron Triggers, además de los eventos tradicionales `fetch()`. Asimismo, podrás probar aplicaciones complejas que interactúan con KV, R2, D1 Queues, Service Bindings y otros productos de Cloudflare con mayor facilidad.

En cuanto a tus pruebas, podrás acceder a las funciones de Vitest como instantáneas, simulacros, temporizadores y espías.

Además de estas ventajas, observarás otras mejoras en la experiencia del desarrollador, como la recarga de módulos en caliente, el modo vigilancia activado por defecto y el almacenamiento aislado por prueba. Esto significa que, a medida que desarrolles y edites tus pruebas, se volverán a ejecutar automáticamente, sin que tengas que reiniciar tu ejecutor de pruebas.

Empieza a probar Workers con Vitest

La forma más sencilla de empezar a probar tus Workers a través de Vitest es iniciar un nuevo proyecto Workers mediante nuestra herramienta create-cloudflare:

npm create cloudflare@latest hello-world -- --type=hello-world

Cuando ejecutes este comando se creará un nuevo proyecto para ti con la integración Workers Vitest ya configurada. También se incluyen una prueba unitaria y una prueba de integración de ejemplo.

Instrucciones manuales de instalación y configuración

Si prefieres instalar y configurar manualmente la integración Workers Vitest, empieza por instalar el paquete @cloudflare/vitest-pool-workers desde npm:

$ npm install --save-dev @cloudflare/vitest-pool-workers

El paquete @cloudflare/vitest-pool-workers tiene una dependencia de emparejamiento con una versión específica de `vitest`. Las versiones modernas de `npm` la instalarán de forma automática, pero te recomendamos que tú también la instales explícitamente. Consulta la guía de inicio para conocer la versión actual compatible. Si utilizas TypeScript, añade @cloudflare/vitest-pool-workers al types de tu tsconfig.json para obtener tipos para el módulo cloudflare:test:

{
  "compilerOptions": {
    "module": "esnext",
    "moduleResolution": "bundler",
    "lib": ["esnext"],
    "types": [
      "@cloudflare/workers-types/experimental",
      "@cloudflare/vitest-pool-workers"
    ]
  }
}

Después, habilita el pool en tu archivo de configuración de Vitest:

// vitest.config.js
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";

export default defineWorkersConfig({
  test: {
    poolOptions: {
      workers: {
        wrangler: { configPath: "./wrangler.toml" },
      },
    },
  },
});

A continuación, define una fecha de compatibilidad posterior a "31-10-2022" y activa el nodejs_compat compatibility flag en tu wrangler.toml:

# wrangler.toml
main = "src/index.ts"
compatibility_date = "2024-01-01"
compatibility_flags = ["nodejs_compat"]

Prueba cualquier recurso exportado desde un Worker

Con la nueva integración de Workers Vitest, puedes probar cualquier recurso desde tu Worker tanto en pruebas unitarias como de integración. Dentro de estas pruebas, también puedes probar recursos conectados como R2, KV y DO, así como aplicaciones que impliquen numerosos Workers.

Escribe pruebas unitarias

En un contexto Workers, una prueba unitaria importa y llama directamente a funciones de tu Worker y luego hace una afirmación sobre sus valores de retorno. Supongamos que tienes un Worker con el siguiente aspecto:

export function add(a, b) {
  return a + b;
}

export default {
  async fetch(request) {
    const url = new URL(request.url);
    const a = parseInt(url.searchParams.get("a"));
    const b = parseInt(url.searchParams.get("b"));
    return new Response(add(a, b));
  }
}

Una vez que hayas configurado e instalado la integración Workers Vitest, puedes realizar una prueba unitaria de este Worker creando un nuevo archivo de prueba llamado `index.spec.js`. con el siguiente código:

import { env, createExecutionContext, waitOnExecutionContext, } from "cloudflare:test";
import { describe, it, expect } from "vitest";
import { add }, worker from "./src";

describe("Hello World worker", () => {
  it(“adds two numbers”, async () => {
    expect(add(2,3).toBe(5);
  });
  it("sends request (unit style)", async () => {
    const request = new Request("http://example.com/?a=3&b=4");
    const ctx = createExecutionContext();
    const response = await worker.fetch(request, env, ctx);
    await waitOnExecutionContext(ctx);
    expect(await response.text()).toMatchInlineSnapshot(`"7"`);
  });
});

Con la integración Workers Vitest, puedes escribir pruebas unitarias como estas para cualquiera de tus Workers.

Escribe pruebas de integración

Mientras que las pruebas unitarias son ideales para probar partes individuales de tu aplicación, las pruebas de integración evalúan diversas unidades de funcionalidad, garantizando que los flujos de trabajo y las funciones operan según lo previsto. Suelen ser más complejas que las pruebas unitarias, pero proporcionan mayor confianza sobre el funcionamiento previsto de tu aplicación. En el contexto de Workers, una prueba de integración envía solicitudes HTTP a tu Worker y realiza afirmaciones sobre las respuestas HTTP.

Con la integración Workers Vitest, puedes ejecutar pruebas de integración importando SELF desde la nueva utilidad cloudflare:test de la siguiente manera:

// test/index.spec.ts
import { SELF } from "cloudflare:test";
import { it, expect } from "vitest";
import "../src";

// an integration test using SELF
it("sends request (integration style)", async () => {
   const response = await SELF.fetch("http://example.com/?a=3&b=4");
   expect(await response.text()).toMatchInlineSnapshot(`"7"`);
});

Al utilizar SELF para las pruebas de integración, tu código Worker se ejecuta en el mismo contexto que el ejecutor de pruebas. Esto significa que puedes utilizar las funciones de simulación para controlar tu Worker.

Prueba distintos escenarios

Tanto si escribes pruebas unitarias como de integración, si tu aplicación utiliza productos de la plataforma para desarrolladores de Cloudflare (por ejemplo, KV, R2, D1, Queues o Durable Objects), puedes probarlos. Para demostrarlo, hemos creado una serie de ejemplos que te ayudarán a empezar a realizar pruebas.

Consigue mejores pruebas, simplificando tu experiencia

Disponer de mejores herramientas facilita las pruebas de tus proyectos desde el principio, lo que se traduce en una mejor calidad y experiencia general para tus usuarios finales. La integración de Workers Vitest mejora esa experiencia, en términos de experiencia del desarrollador, y simplifica las pruebas de toda tu aplicación.

A partir de aquí hablaremos sobre cómo desarrollamos esta nueva integración, analizaremos los aspectos internos del funcionamiento de Vitest, los problemas que encontramos al intentar que un marco funcionara dentro de nuestro entorno de ejecución y, finalmente, explicaremos cómo lo resolvimos y la mejora de DX que descubrimos.

Cómo funciona Vitest habitualmente

Cuando inicias la CLI de Vitest, primero recopila y secuencia todos tus archivos de prueba. Por defecto, Vitest utiliza un grupo de "hilos", que genera hilos workers de Node.js para aislar y ejecutar pruebas en paralelo. Cada hilo recibe un archivo de prueba para ejecutar, solicitando y evaluando dinámicamente el código según sea necesario. Cuando el ejecutor de pruebas importa un módulo, envía una solicitud al "servidor Vite Node" del host, que devolverá o bien código JavaScript sin procesar transformado por Vite, o bien una ruta de módulo externa. Si se devuelve código sin procesar, se ejecutará mediante la node:vm runInThisContext() Si se devuelve una ruta de módulo, se importará utilizando la función dinámica import(). La transformación del código de usuario con Vite permite la recarga de módulos en caliente (HMR). Cuando un módulo cambia, se invalida en la caché de módulos y se devolverá una nueva versión la próxima vez que se importe.

overview of Vitest’s architecture using Miniflare v2’s environments
(Visión general de la arquitectura de Vitest utilizando los entornos de Miniflare v2)

Miniflare es un simulador totalmente local para la plataforma de desarrolladores de Cloudflare. Miniflare v2 proporcionaba un entorno personalizado para Vitest que te permitía ejecutar tus pruebas dentro del espacio aislado de Workers. De esta manera, podías importar y llamar a cualquier función que utilizara las API de ejecución de Workers en tus pruebas. No estabas limitado a pruebas de integración que solo enviaban y recibían solicitudes HTTP. Además, este entorno proporcionaba almacenamiento aislado por prueba, anulando automáticamente cualquier cambio realizado al final de cada prueba. En Miniflare v2, este entorno era relativamente sencillo de implementar. Ya habíamos vuelto a implementar las API del entorno de ejecución de Workers en un entorno Node.js, y podíamos inyectarlas utilizando las API de Vitest en el ámbito global del ejecutor de pruebas.

En cambio, Miniflare v3 ejecuta tu código Worker dentro del mismo entorno de ejecución workerd que Cloudflare utiliza en producción. La ejecución de pruebas directamente en workerd planteó un desafío: workerd se ejecuta en su propio proceso, separado del hilo worker de Node.js, y no es posible hacer referencia a clases JavaScript a través de un límite de proceso.

problem with Miniflare v3, the runtime APIs are defined in a separate process to the test environments, and JavaScript objects cannot cross process boundaries
(Problema con Miniflare v3: las API del entorno de ejecución se definen en un proceso separado de los entornos de prueba, y los objetos JavaScript no pueden cruzar los límites de los procesos)

Cómo resolvimos el problema de los pools personalizados

En su lugar, utilizamos la función pools personalizados de Vitest para usar el ejecutor de pruebas en Cloudflare Workers que se ejecuta localmente con workerd. Un pool recibe archivos de prueba para ejecutar y decide cómo ejecutarlos. Al usar el ejecutor dentro de workerd, las pruebas tienen acceso directo a las API del entorno de ejecución de Workers, ya que se están ejecutando en un Worker. Utilizamos WebSockets para enviar y recibir mensajes RPC serializables entre el host Node.js y el proceso workerd. Ten en cuenta que aquí estamos ejecutando exactamente el mismo código de ejecución de pruebas diseñado originalmente para un contexto Node dentro de un Worker. Esto significa que nuestro Worker tiene que proporcionar los módulos incorporados de Node, el soporte para la evaluación dinámica del código y la carga de módulos arbitrarios desde el disco con un comportamiento de resolución de Node. El indicador de compatibilidad nodejs_compat da soporte a algunos de los módulos incorporados de Node, pero no resuelve el resto de nuestros problemas. Para eso, tuvimos que poner en práctica nuestra creatividad...

our solution for Miniflare v3, make the tests run in workerd, and use WebSockets for communication
(Nuestra solución para Miniflare v3: hacer que las pruebas se ejecuten en workerd, y usar WebSockets para la comunicación)

Evaluación de código dinámico

Por razones de seguridad, el entorno de ejecución de Cloudflare Workers no permite la evaluación de código dinámico mediante eval() o new Function(). También requiere que todos los módulos se definan con antelación antes de que comience la ejecución. El ejecutor de pruebas no sabe qué código ejecutar hasta que empezamos a ejecutar las pruebas, así que sin eliminar estas restricciones, no tenemos forma de ejecutar el código JavaScript sin procesar transformado por Vite ni de importar módulos arbitrarios desde el disco. Afortunadamente, el código que solo se ejecuta localmente, como las pruebas, tiene un modelo de seguridad mucho más moderado que el código implementado. Para admitir las pruebas locales y otros casos de uso específicos del desarrollo, como la nueva API de tiempo de ejecución de Vite, hemos añadido "unsafe-eval bindings" y "module-fallback services" a workerd.

El método "unsafe-eval bindings" solo proporciona acceso local a la función eval() y a los constructores new Function()/new AsyncFunction()/new WebAssembly.Module() . Cuando los exponemos a través de un enlace, mantenemos el control sobre qué código tiene acceso a estas funciones.

// Type signature for unsafe-eval bindings
interface UnsafeEval {
  eval(script: string, name?: string): unknown;
  newFunction(script: string, name?: string, ...args: string[]): Function;
  newAsyncFunction(script: string, name?: string, ...args: string[]): AsyncFunction;
  newWasmModule(src: BufferSource): WebAssembly.Module;
}

Con el método unsafe-eval binding eval(), pudimos implementar un polyfill para la función requerida vm.runInThisContext(). Aunque también podríamos implementar la carga de módulos arbitrarios desde el disco utilizando enlaces unsafe-eval, esto nos obligaría a volver a crear el sistema de resolución de módulos de workerd en JavaScript. En su lugar, permitimos que los workers se configuren con servicios de reserva de módulos. Si se activan, las importaciones que workerd no puede resolver se convierten en solicitudes HTTP para el servicio reserva. Estas incluyen el especificador, la referencia y si se trata de import o require. El servicio puede responder con una definición del módulo, o una redirección a otra ubicación si la ubicación resuelta no coincide con el especificador. Las solicitudes síncronas procedentes de require bloquearán el hilo principal hasta que se resuelva el módulo. El servicio reserva del pool Workers Vites implementa una resolución similar a Node con interoperabilidad del estilo Node entre módulos CommonJS y ES.

Durable Objects como ejecutores de prueba

Ahora que podemos ejecutar e importar código arbitrario, el siguiente paso es conseguir que el hilo de Vitest Worker se ejecute dentro de workerd. Cada solicitud entrante tiene su propio contexto de solicitud. Para mejorar el rendimiento general, los objetos de E/S como flujos, cuerpos de solicitud/respuesta y WebSockets creados en un contexto de solicitud no se pueden utilizar desde otro. Esto significa que si queremos utilizar un WebSocket para RPC entre el pool y nuestros procesos workerd, tenemos que asegurarnos de que el WebSocket solo se utiliza desde un contexto de solicitud. Para coordinar esto, definimos un singleton Durable Objects para aceptar la conexión RPC y ejecutar pruebas desde ahí. Las funciones que utilicen RPC, como la resolución de módulos, la notificación de resultados y el registro en consola, utilizarán siempre este singleton. Utilizamos el sistema de "magic proxy" de Miniflare para obtener una referencia al stub del singleton en Node.js, y enviarle directamente una solicitud de actualización WebSocket. Después de añadir unos cuantos polyfills más de Node.js, y un módulo básico cloudflare:test para proporcionar acceso a los enlaces y una función para crear ExecutionContext, ¡podemos escribir pruebas unitarias básicas de Workers! 🎉

architecture of the Workers Vitest Pool
(Arquitectura del pool de Workers Vitest)

Pruebas de integración con recarga de módulos en caliente

Además de las pruebas unitarias, admitimos las pruebas de integración con un enlace de servicio especial SELF en el módulo cloudflare:test. Esto apunta a un controlador especial export default { fetch(...) {...} } que utiliza Vite para importar el módulo main de tu Worker.

El uso de la canalización de transformación de Vite significa que tu controlador obtiene la recarga en caliente de módulos ¡gratis! Cuando se actualiza el código, se invalida la caché del módulo, se vuelven a ejecutar las pruebas y las solicitudes posteriores se ejecutarán con el nuevo código. El mismo enfoque para ajustar los controladores de código de usuario se aplica también a Durable Objects, proporcionando las mismas ventajas de HMR.

Las pruebas de integración se pueden escribir llamando a SELF.fetch(), que enviará un evento fetch() a tu código de usuario en el mismo ámbito global que tu prueba, pero bajo un contexto de solicitud diferente. Esto significa que los simuladores globales se aplican a la ejecución de tu Worker, al igual que las restricciones de duración del contexto de solicitud. En particular, si olvidas llamar a ctx.waitUntil(), recibirás un mensaje de error apropiado. Esto no ocurriría si llamaras directamente a tu controlador Worker en una prueba unitaria, ya que estarías ejecutando bajo el contexto de solicitud Durable Objects del singleton ejecutor, cuya duración se amplía automáticamente.

// test/index.spec.ts
import { SELF } from "cloudflare:test";
import { it, expect } from "vitest";
import "../src/index";

it("sends request", async () => {
   const response = await SELF.fetch("https://example.com");
   expect(await response.text()).toMatchInlineSnapshot(`"body"`);
});

Almacenamiento aislado por prueba

La mayoría de las aplicaciones Workers tendrán al menos un enlace a un servicio de almacenamiento de Cloudflare, como KV, R2 o D1. Lo ideal es que las pruebas sean autónomas y se puedan ejecutar en cualquier orden o por sí solas. Para que esto sea posible, las escrituras en el almacenamiento se deben anular al final de cada prueba, para que las lecturas de otras pruebas no se vean afectadas. Aunque es posible hacerlo manualmente, puede ser complicado llevar la cuenta de todas las escrituras y anularlas en el orden correcto. Por ejemplo, observa las dos funciones siguientes:

// helpers.ts
interface Env {
  NAMESPACE: KVNamespace;
}
// Get the current list stored in a KV namespace
export async function get(env: Env, key: string): Promise<string[]> {
  return await env.NAMESPACE.get(key, "json") ?? [];
}
// Add an item to the end of the list
export async function append(env: Env, key: string, item: string) {
  const value = await get(env, key);
  value.push(item);
  await env.NAMESPACE.put(key, JSON.stringify(value));
}

Si quisiéramos probar estas funciones, podríamos escribir lo que ves a continuación. Ten en cuenta que tenemos que hacer un seguimiento de todas las claves en las que podríamos escribir, y restaurar sus valores al final de las pruebas, aunque fallen.

// helpers.spec.ts
import { env } from "cloudflare:test";
import { beforeAll, beforeEach, afterEach, it, expect } from "vitest";
import { get, append } from "./helpers";

let startingList1: string | null;
let startingList2: string | null;
beforeEach(async () => {
  // Store values before each test
  startingList1 = await env.NAMESPACE.get("list 1");
  startingList2 = await env.NAMESPACE.get("list 2");
});
afterEach(async () => {
  // Restore starting values after each test
  if (startingList1 === null) {
    await env.NAMESPACE.delete("list 1");
  } else {
    await env.NAMESPACE.put("list 1", startingList1);
  }
  if (startingList2 === null) {
    await env.NAMESPACE.delete("list 2");
  } else {
    await env.NAMESPACE.put("list 2", startingList2);
  }
});

beforeAll(async () => {
  await append(env, "list 1", "one");
});

it("appends to one list", async () => {
  await append(env, "list 1", "two");
  expect(await get(env, "list 1")).toStrictEqual(["one", "two"]);
});

it("appends to two lists", async () => {
  await append(env, "list 1", "three");
  await append(env, "list 2", "four");
  expect(await get(env, "list 1")).toStrictEqual(["one", "three"]);
  expect(await get(env, "list 2")).toStrictEqual(["four"]);
});

Esto es algo más fácil con el enlace onTestFinished(), que se ha implementado recientemente, pero sigues necesitando recordar en qué claves se escribieron, o enumerarlas al inicio/final de las pruebas. También tendrías que gestionarlo para KV, R2, Durable Objects, cachés y cualquier otro servicio de almacenamiento que utilices. Lo ideal sería que el marco de pruebas gestionara todo esto por ti.

Eso es exactamente lo que hace el pool Workers Vitest con la opción isolatedStorage, que está activada por defecto. Cualquier escritura para almacenamiento que se realice en una prueba se anula automáticamente (como si fuera magia) al final de la prueba. Para admitir la inicialización de elementos de datos en los enlaces beforeAll(), incluidos los de los bloques describe() anidados, se utiliza una pila. Antes de cada conjunto o prueba, se introduce un nuevo marco en la pila de almacenamiento. Todas las escrituras realizadas por la prueba o los enlaces asociados beforeEach()/afterEach() se escriben en el marco. Después de cada conjunto o prueba, el marco superior se extrae de la pila de almacenamiento, anulando cualquier escritura.

Storage stack frames created with isolated storage enabled
(Marcos de pila de almacenamiento creados con el almacenamiento aislado activado)

Miniflare implementa simuladores para servicios de almacenamiento, además de Durable Objects, con un almacén de blobs independiente. Cuando se ejecuta localmente, workerd utiliza SQLite para el almacenamiento de Durable Objects. Para implementar el almacenamiento aislado, aplicamos una pila almacenada en un disco de archivos de bases de datos .sqlite a través de copias de seguridad en operaciones "push", y restauración de copias de seguridad en operaciones "pop". Los blobs almacenados en el almacén separado se conservan mediante operaciones de pila, y se limpian al final de cada ejecución de prueba. Aunque esto funciona, implica copiar un volumen importante de archivos .sqlite. De cara al futuro, nos gustaría explorar el uso de SAVEPOINTS de SQLite para conseguir una solución más eficaz.

Simulación de solicitud declarativa

Además del almacenamiento, la mayoría de los Workers realizan solicitudes de salida fetch(). Para las pruebas, suele ser útil simular las respuestas a estas solicitudes. Miniflare ya te permite especificar un MockAgent de undici para enrutar todas las solicitudes a través del mismo. La clase MockAgent proporciona una interfaz declarativa para especificar las solicitudes a simular y las correspondientes respuestas a devolver. Esta API es relativamente sencilla, pero lo suficientemente flexible para casos de uso avanzados. Proporcionamos una instancia de MockAgent como fetchMock en el módulo cloudflare:test.

import { fetchMock } from "cloudflare:test";
import { beforeAll, afterEach, it, expect } from "vitest";

beforeAll(() => {
  // Enable outbound request mocking...
  fetchMock.activate();
  // ...and throw errors if an outbound request isn't mocked
  fetchMock.disableNetConnect();
});
// Ensure we matched every mock we defined
afterEach(() => fetchMock.assertNoPendingInterceptors());

it("mocks requests", async () => {
  // Mock the first request to `https://example.com`
  fetchMock
    .get("https://example.com")
    .intercept({ path: "/" })
    .reply(200, "body");

  const response = await fetch("https://example.com/");
  expect(await response.text()).toBe("body");
});

Para implementarlo, incluimos una versión reducida de undici que solo contenía el código de MockAgent. A continuación, creamos un Dispatcher personalizado de undici que utilizaba la función global fetch() del Worker en lugar de la implementación HTTP integrada de undici basada en llhttp y node:net.

Cómo probar directamente Durable Objects

Por último, el entorno personalizado Vitest de Miniflare v2 proporcionó soporte para acceder directamente a los métodos de instancia y al estado de Durable Objects en las pruebas. Este enfoque te permitía realizar pruebas unitarias de Durable Objects como con cualquier otra clase de JavaScript. Podías simular métodos y propiedades concretos, o llamar inmediatamente a controladores específicos como alarm(). Para implementarlo en workerd, nos basamos en nuestro actual contenedor de Durable Objects de usuario para las transformaciones Vite y la recarga de módulos en caliente. Cuando llamas a la función runInDurableObject(stub, callback) desde cloudflare:test, almacenamos callback en una caché global y enviamos una solicitud especial fetch() a stub que es interceptada por el contenedor. El contenedor ejecuta la callback en el contexto de solicitud de Durable Objects, y almacena el resultado en la misma caché. A continuación, runInDurableObject() lee de esta caché y devuelve el resultado.

Ten en cuenta que esto supone que Durable Objects se está ejecutando en el mismo entorno aislado que la llamada a runInDurableObject(). Aunque no es el caso de Durable Objects del mismo Worker que se ejecuta localmente, significa que no se puede acceder directamente a los Durable Objects definidos en los workers auxiliares.

Pruébalo

Nos complace anunciar el paquete @cloudflare/vitest-pool-workers en npm que mejorará tu experiencia de pruebas.

Lee la guía "Cómo escribir tu primera prueba" y empieza a escribir pruebas unitarias y de integración hoy mismo. Si has estado escribiendo pruebas utilizando una de nuestras opciones anteriores, nuestra guía de migración unstable_dev o nuestra guía de migración Miniflare 2 deberían explicarte las diferencias clave y ayudarte a migrar tus pruebas rápidamente.

Si estás experimentando problemas o tienes sugerencias de mejora, infórmanos en nuestro repositorio de GitHub o ponte en contacto con nosotros a través de nuestra plataforma Discord para desarrolladores.

Proteggiamo intere reti aziendali, aiutiamo i clienti a costruire applicazioni su scala Internet in maniera efficiente, acceleriamo siti Web e applicazioni Internet, respingiamo gli attacchi DDoS, teniamo a bada gli hacker e facilitiamo il tuo percorso verso Zero Trust.

Visita 1.1.1.1 da qualsiasi dispositivo per iniziare con la nostra app gratuita che rende la tua rete Internet più veloce e sicura.

Per saperne di più sulla nostra missione di contribuire a costruire un Internet migliore, fai clic qui. Se stai cercando una nuova direzione professionale, dai un'occhiata alle nostra posizioni aperte.
Cloudflare Workers (ES)Developers (ES)Testing (ES)Español

Segui su X

Brendan Coll|@_mrbbot
Adam Murray|@admah
Cloudflare|@cloudflare

Post correlati