Skip to main content

No project description provided

Project description

Mountaineer Header

Move fast. Climb mountains. Don't break things.

Mountaineer 🏔️ is a framework to easily build webapps in Python and React. If you've used either of these languages before for development, we think you'll be right at home.

Main Features

Each framework has its own unique features and tradeoffs. Mountaineer focuses on developer productivity above all else, with production speed a close second.

  • 📝 Typehints up and down the stack: frontend, backend, and database
  • 🎙️ Trivially easy client<->server communication, data binding, and function calling
  • 🌎 Optimized server rendering for better accessibility and SEO
  • 🏹 Static analysis of web pages for strong validation: link validity, data access, etc.
  • 🤩 Skip the API or Node.js server just to serve frontend clients

We built Mountaineer out of a frustration that we were reinventing the webapp wheel time and time again. We love Python for backend development and the interactivity of React for frontend UX. But they don't work seamlessly together without a fair amount of glue. So: we built the glue. While we were at it, we embedded a V8 engine to provide server-side rendering, added conventions for application configuration, built native Typescript integrations, and more. Our vision is for you to import one slim dependency and you're off to the races.

We're eager for you to give Mountaineer a try, and equally devoted to making you successful if you like it. File an Issue if you see anything unexpected or if there's a steeper learning curve than you expect. There's much more to do - and we're excited to do it together.

~ Pierce

Getting Started

New Project

To get started as quickly as possible, we bundle a project generator that sets up a simple project after a quick Q&A. Make sure you have pipx installed.

$ pipx run create-mountaineer-app

? Project name [my-project]: my_webapp
? Author [Pierce Freeman <pierce@freeman.vc>] Default
? Use poetry for dependency management? [Yes] Yes
? Create stub MVC files? [Yes] Yes
? Use Tailwind CSS? [Yes] Yes
? Add editor configuration? [vscode] vscode

Mountaineer projects all follow a similar structure. After running this CLI you should see a new folder called my_webapp, with folders like the following:

my_webapp
  /controllers
    /home.py
  /models
    /mymodel.py
  /views
    /app
      /home
        /page.tsx
      /layout.tsx
    /package.json
    /tsconfig.json
  /app.py
  /cli.py
pyproject.toml
poetry.lock

Every service file is nested under the my_webapp root package. Views are defined in a disk-based hierarchy (views) where nested routes are in nested folders. This folder acts as your React project and is where you can define requirements and build parameters in package.json and tsconfig.json. Controllers are defined nearby in a flat folder (controllers) where each route is a separate file. Everything else is just standard Python code for you to modify as needed.

Development

If you're starting a new application from scratch, you'll typically want to create your new database tables. Make sure you have postgres running. We bundle a docker compose file for convenience with create-mountaineer-app.

docker compose up -d
poetry run createdb

Of course you can also use an existing database instance, simply configure it in the .env file in the project root.

Mountaineer relies on watching your project for changes and doing progressive compilation. We provide a few CLI commands to help with this.

While doing development work, you'll usually want to preview the frontend and automatically build dependent files. You can do this with:

$ poetry run runserver

INFO:     Started server process [93111]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:5006 (Press CTRL+C to quit)

Navigate to http://127.0.0.1:5006 to see your new webapp running.

Or, if you just want to watch the source tree for changes without hosting the server. Watching will allow your frontend to pick up API definitions from your backend controllers:

$ poetry run watch

Both of these CLI commands are specified in your project's cli.py file.

Walkthrough

Below we go through some of the unique aspects of Mountaineer. Let's create a simple Todo list where we can add new items.

For the purposes of this walkthrough we assume your project is generated with create-mountaineer-app and you've skipped MVC stub files. If not, you'll have to delete some of the pre-existing files.

Let's get started by creating the data models that will persist app state to the database. These definitions are effectively Pydantic schemas that will be bridged to the database via SQLModel.

# my_webapp/models/todo.py

from mountaineer.database import SQLModel, Field
from uuid import UUID, uuid4

class TodoItem(SQLModel, table=True):
    id: UUID = Field(default_factory=uuid4, primary_key=True)

    description: str
    completed: bool = False

Update the index file as well:

# my_webapp/models/__init__.py

from .todo import TodoItem # noqa: F401

Make sure you have a Postgres database running. We bundle a docker compose file for convenience with create-mountaineer-app. Launch it in the background and create the new database tables from these code definitions:

docker compose up -d
poetry run createdb
poetry run runserver

Great! At this point we have our database tables created and have a basic server running. We next move to creating a new controller, since this will define which data you can push and pull to your frontend.

# my_webapp/controllers/home.py

from mountaineer import sideeffect, ControllerBase, RenderBase
from mountaineer.database import DatabaseDependencies

from fastapi import Request, Depends
from mountaineer.database.session import AsyncSession
from sqlmodel import select

from my_webapp.models.todo import TodoItem

class HomeRender(RenderBase):
    client_ip: str
    todos: list[TodoItem]

class HomeController(ControllerBase):
    url = "/"
    view_path = "/app/home/page.tsx"

    async def render(
        self,
        request: Request,
        session: AsyncSession = Depends(DatabaseDependencies.get_db_session)
    ) -> HomeRender:
        todos = (await session.exec(select(TodoItem))).all()

        return HomeRender(
            client_ip=(
                request.client.host
                if request.client
                else "unknown"
            ),
            todos=todos
        )

The only three requirements of a controller are setting the:

  • URL
  • View path
  • Initial data payload

This render() function is a core building block of Mountaineer. All Controllers need to have one. It defines all the data that your frontend will need to resolve its view. This particular controller retrieves all Todo items from the database, alongside the user's current IP.

[!TIP] render() functions accepts all parameters that FastAPI endpoints do: paths, query parameters, and dependency injected functions. Right now we're just grabbing the Request object to get the client IP.

Note that the database session is provided via dependency injection, which plug-and-plays with FastAPI's Depends syntax. The standard library provides two main dependency providers:

  • mountaineer.CoreDependencies: helper functions for configurations and general dependency injection
  • mountaineer.database.DatabaseDependencies: helper functions for database lifecycle and management

Now that we've newly created this controller, we wire it up to the application. This registers it for display when you load the homepage.

# my_webapp/app.py
from mountaineer.app import AppController
from mountaineer.js_compiler.postcss import PostCSSBundler
from mountaineer.render import LinkAttribute, Metadata

from my_webapp.config import AppConfig
from my_webapp.controllers.home import HomeController

controller = AppController(
    config=AppConfig(),
    global_metadata=Metadata(
        links=[LinkAttribute(rel="stylesheet", href="/static/app_main.css")]
    ),
    custom_builders=[
        PostCSSBundler(),
    ],
)

controller.register(HomeController())

Let's move over to the frontend.

/* my_webapp/views/app/home/page.tsx */

import React from "react";
import { useServer, ServerState } from "./_server/useServer";

const CreateTodo = ({ serverState }: { serverState: ServerState }) => {
  return (
    <div className="flex gap-x-4">
      <input
        type="text"
        className="grow rounded border-2 border-gray-200 px-4 py-2"
      />
      <button className="rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700">
        Create
      </button>
    </div>
  );
};

const Home = () => {
  const serverState = useServer();

  return (
    <div className="mx-auto max-w-2xl space-y-8 p-8 text-2xl">
      <p>
        Hello {serverState.client_ip}, you have {serverState.todos.length} todo
        items.
      </p>
      <CreateTodo serverState={serverState} />
      {
        /* Todo items are exposed as typehinted Typescript interfaces */
        serverState.todos.map((todo) => (
          <div key={todo.id} className="rounded border-2 border-gray-200 p-4">
            <div>{todo.description}</div>
          </div>
        ))
      }
    </div>
  );
};

export default Home;

We define a simple view to show the data coming from the backend. To accomplish this conventionally, we'd need to wire up an API layer, a Node server, or format the page with Jinja templates.

Here instead we use our automatically generated useServer() hook. This hook payload will provide all the HomeRender fields as properties of serverState. And it's available instantly on page load without any roundtrip fetches. Also - if your IDE supports language servers (which most do these days), you should see the fields auto-suggesting for serverState as you type.

IDE Typehints

If you access this in your browser at localhost:5006/ we can see our welcome message, but we can't really do anything with the todos yet. Let's add some interactivity.

[!TIP] Try disabling Javascript in your browser. The page will still render as-is with all variables intact, thanks to our server-side rendering.

Server-side rendering

What good is todo list that doesn't get longer? We define a add_todo function that accepts a pydantic model NewTodoRequest, which defines the required parameters for a new todo item. We then cast this to a database object and add it to the postgres table.

# my_webapp/controllers/home.py

from pydantic import BaseModel

class NewTodoRequest(BaseModel):
    description: str

class HomeController(ControllerBase):
    ...

    @sideeffect
    async def add_todo(
        self,
        payload: NewTodoRequest,
        session: AsyncSession = Depends(DatabaseDependencies.get_db_session)
    ) -> None:
        new_todo =  TodoItem(description=payload.description)
        session.add(new_todo)
        await session.commit()

The important part here is the @sideeffect. Once you create a new Todo item, the previous state on the frontend is outdated. It will only show the todos before you created a new one. That's not what we want in an interactive app. This decorator indicates that we want the frontend to refresh its data, since after we update the todo list on the server the client state will be newly outdated.

Mountaineer detects the presence of this sideeffect function and analyzes its signature. It then exposes this to the frontend as a normal async function.

/* my_webapp/views/app/home/page.tsx */

import React, { useState } from "react";
import { useServer } from "./_server/useServer";

/* Replace the existing CreateTodo component definition you have */
const CreateTodo = ({ serverState }: { serverState: ServerState }) => {
  const [newTodo, setNewTodo] = useState("");

  return (
    <div className="flex gap-x-4">
      <input
        type="text"
        className="grow rounded border-2 border-gray-200 px-4 py-2"
        value={newTodo}
        onChange={(e) => setNewTodo(e.target.value)}
      />
      <button
        className="rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700"
        onClick={
          /* Here we call our sideeffect function */
          async () => {
            await serverState.add_todo({
              requestBody: {
                description: newTodo,
              },
            });
            setNewTodo("");
          }
        }
      >
        Create
      </button>
    </div>
  );
};

...

export default Home;

useServer() exposes our add_todo function so we can call our backend directly from our frontend. Also notice that we don't have to read or parse the output value of this function to render the new todo item to the list. Since the function is marked as a sideeffect, the frontend will automatically refresh its data after the function is called.

Go ahead and load it in your browser. If you open up your web tools, you can create a new Todo and see POST requests sending data to the backend and receiving the current server state. The actual data updates and merging happens internally by Mountaineer.

Getting Started Final TODO App

Getting Started Final TODO App

You can use these serverState variables anywhere you'd use dynamic React state variables (useEffect, useCallback, etc). But unlike React state, these variables are automatically updated when a relevant sideeffect is triggered.

And that's it. We've just built a fully interactive web application without having to worry about an explicit API. You specify the data model and actions on the server and the appropriate frontend hooks are generated and updated automatically. It gives you the power of server rendered html and the interactivity of a virtual DOM, without having to compromise on complicated data mutations to keep everything in sync.

Learn More

We have additional documentation that does more of a technical deep dive on different features of Mountaineer. Check out mountaineer.sh.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mountaineer-0.5.2.dev4.tar.gz (3.2 MB view details)

Uploaded Source

Built Distributions

mountaineer-0.5.2.dev4-cp312-none-win_amd64.whl (17.2 MB view details)

Uploaded CPython 3.12 Windows x86-64

mountaineer-0.5.2.dev4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (20.6 MB view details)

Uploaded CPython 3.12 manylinux: glibc 2.17+ x86-64

mountaineer-0.5.2.dev4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (20.2 MB view details)

Uploaded CPython 3.12 manylinux: glibc 2.17+ ARM64

mountaineer-0.5.2.dev4-cp312-cp312-macosx_11_0_arm64.whl (17.8 MB view details)

Uploaded CPython 3.12 macOS 11.0+ ARM64

mountaineer-0.5.2.dev4-cp312-cp312-macosx_10_12_x86_64.whl (18.7 MB view details)

Uploaded CPython 3.12 macOS 10.12+ x86-64

mountaineer-0.5.2.dev4-cp311-none-win_amd64.whl (17.2 MB view details)

Uploaded CPython 3.11 Windows x86-64

mountaineer-0.5.2.dev4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (20.6 MB view details)

Uploaded CPython 3.11 manylinux: glibc 2.17+ x86-64

mountaineer-0.5.2.dev4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (20.2 MB view details)

Uploaded CPython 3.11 manylinux: glibc 2.17+ ARM64

mountaineer-0.5.2.dev4-cp311-cp311-macosx_11_0_arm64.whl (17.8 MB view details)

Uploaded CPython 3.11 macOS 11.0+ ARM64

mountaineer-0.5.2.dev4-cp311-cp311-macosx_10_12_x86_64.whl (18.7 MB view details)

Uploaded CPython 3.11 macOS 10.12+ x86-64

mountaineer-0.5.2.dev4-cp310-none-win_amd64.whl (17.2 MB view details)

Uploaded CPython 3.10 Windows x86-64

mountaineer-0.5.2.dev4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (20.6 MB view details)

Uploaded CPython 3.10 manylinux: glibc 2.17+ x86-64

mountaineer-0.5.2.dev4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (20.2 MB view details)

Uploaded CPython 3.10 manylinux: glibc 2.17+ ARM64

mountaineer-0.5.2.dev4-cp310-cp310-macosx_11_0_arm64.whl (17.8 MB view details)

Uploaded CPython 3.10 macOS 11.0+ ARM64

mountaineer-0.5.2.dev4-cp310-cp310-macosx_10_12_x86_64.whl (18.7 MB view details)

Uploaded CPython 3.10 macOS 10.12+ x86-64

File details

Details for the file mountaineer-0.5.2.dev4.tar.gz.

File metadata

  • Download URL: mountaineer-0.5.2.dev4.tar.gz
  • Upload date:
  • Size: 3.2 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/5.1.0 CPython/3.12.5

File hashes

Hashes for mountaineer-0.5.2.dev4.tar.gz
Algorithm Hash digest
SHA256 edc6de7b0596cc83974d862e0296e91349038b00c2db00d09d50f7f88bdc9ceb
MD5 d4e6668f9182b2fa320f0ea25d9190bc
BLAKE2b-256 7f92deb5de7c14cc9b637e0d8d0426c3fbac3cdc7ecaa17af7eaa5718326fc06

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.2.dev4-cp312-none-win_amd64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev4-cp312-none-win_amd64.whl
Algorithm Hash digest
SHA256 5a76fe2ae0bf737f827129b789ab776c6ab6975176897edca0ab006e2a32160c
MD5 075ec6602be5a00520bb0ec6c66ee8fe
BLAKE2b-256 85fcb9921d7963f68e9325391622e2aafc9c9452ca73ec263dab3bef4de2a538

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.2.dev4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 08115ad73e02f7a68f1797cdf18bf412fc9d61f2398010ee490252327fde7117
MD5 c74a3261bfe3501d0dd06798e71e0169
BLAKE2b-256 f0a1384d8eea503ef065caf61c2cd543c095b81e59117dc3f5abfb0a355463a5

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.2.dev4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 d7e26d7c3cba2b7981931bcc4c2deb4d1c907eabf5eed23a831839f905d63498
MD5 21a0ad6be4958010617cd290a2ad0237
BLAKE2b-256 d476e9e34d8631f378124b293cdd9abb5eb0b4d521293236fb5996b5873622a6

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.2.dev4-cp312-cp312-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev4-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 c527c40fc713d21328e307105b90ca80e3e113dd88ee0273b06420c4e707c5cd
MD5 f4e164375beb64e6c7643b3f8d4d7962
BLAKE2b-256 d4cb1a6590e06dec7784ffcfc2010166562dcc665a753c44933664644f366584

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.2.dev4-cp312-cp312-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev4-cp312-cp312-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 4d25524db24d62f8cc7f0acd0b2a81f3a8e8bfd27d164ce7e49d34ee0d87352e
MD5 b31ccf788515cf5f019381200723529a
BLAKE2b-256 4eee987b40a7c8048d161514c6913dc58829bffa9db26db28980a9d0478641c6

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.2.dev4-cp311-none-win_amd64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev4-cp311-none-win_amd64.whl
Algorithm Hash digest
SHA256 0a2433b3c9f13f0814863a1e56dee57214e9277f78c4128b7f206aff6f0a9308
MD5 f1d077df62715e6f8bb1a02d61f00564
BLAKE2b-256 e655e95cbaf339ce3d6d75ffe3726734510dbdb868468bde5ae9f3a953a09230

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.2.dev4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 4c298958593ee1c47ec25356cf32c1e6480dfdf9fc3d4b39d340276e03b98467
MD5 47aed9f2274eaeca7d66a7e8573daacb
BLAKE2b-256 2abca3a8aaf1dd723170d1d530ea693e5c8f56f365ba835ac6d82c5a0656855e

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.2.dev4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 79c06c23b5b0e15a2dd45a81abfe9305ea07395a4881381f73c9c78af3752fdd
MD5 6c12d651b68b6f071fc2b2db79886df9
BLAKE2b-256 c5b36362fc78b2c30d8999221774b0af43629c07e10004ee6a4937f52ddaa326

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.2.dev4-cp311-cp311-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev4-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 fbc0e572de59ae77885f8475416e4287787bc4f3a97fe794adcc13d7fa778220
MD5 baed5aaa2b8c6d862f9bfce2d8d7efb8
BLAKE2b-256 262c7f9b86bf16f3ff9b481cd486a58ac589e94280ed4859dd11a4b565bfeff7

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.2.dev4-cp311-cp311-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev4-cp311-cp311-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 1aa9ce1824337324b3da4ffaf3417fad221d7d504761b2eab0a2a923211e2b83
MD5 c655676f4f0404bdeae7c3ecd5a564e4
BLAKE2b-256 5843bc49f1f8082af5b9175086369d6225786b21f84d9848b7ce120695f5db55

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.2.dev4-cp310-none-win_amd64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev4-cp310-none-win_amd64.whl
Algorithm Hash digest
SHA256 1e8e85c0ad6abc0cc15a2489e29ba7e0cc1866e31ca60eea9e0bd90d3871690c
MD5 8eef62ecb7ee5d6f5897f969065b9953
BLAKE2b-256 ac586abbfdfa53410bd553a8262aeb219cb1a033663f4f3213c8c154849206ef

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.2.dev4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 697b9eefa9234bc7a6673927efb1801c5275669fcfdd9cfb5ddadc7015e070df
MD5 393ec2506d8ca46f79825f5c22e1e11c
BLAKE2b-256 5c2da25fe30c04919bf5bd68864b1c166dafb3a69e44b2f3253361fa96e6af4c

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.2.dev4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 fbb70743dba789a5addeb36827256216016378daa69400cac72ed8dc2522caf0
MD5 1b2cbaa3fb3b9d4941d652044c1b68e1
BLAKE2b-256 a8a8af012d24901dd6e3071df14b0b38d6b6cabaaf12e9ea60ad05e11a644308

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.2.dev4-cp310-cp310-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev4-cp310-cp310-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 e8083eb792479bc61801d7067420986e8ae3462d6492098207fa97c3a0225de3
MD5 4316c58a119562eac73763a32719e1aa
BLAKE2b-256 ed91a28e94b3d4f62d71e823e2006c9b593300eedb35180dcf06b4b9a6ad4020

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.2.dev4-cp310-cp310-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev4-cp310-cp310-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 486a1a26f5ca42e5e045b1c5ce32d6452ea870e88d5c421f17a0705d5d51c59f
MD5 25c4ad5c746446083a1f08879a738a5c
BLAKE2b-256 fe45703014990b11a7f54825e72fde178e0f4bc6ae42c248bd711106ab1bd8c8

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page