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.client_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.7.0.dev5.tar.gz (3.3 MB view details)

Uploaded Source

Built Distributions

mountaineer-0.7.0.dev5-cp312-none-win_amd64.whl (17.3 MB view details)

Uploaded CPython 3.12 Windows x86-64

mountaineer-0.7.0.dev5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (20.7 MB view details)

Uploaded CPython 3.12 manylinux: glibc 2.17+ x86-64

mountaineer-0.7.0.dev5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (20.3 MB view details)

Uploaded CPython 3.12 manylinux: glibc 2.17+ ARM64

mountaineer-0.7.0.dev5-cp312-cp312-macosx_11_0_arm64.whl (17.9 MB view details)

Uploaded CPython 3.12 macOS 11.0+ ARM64

mountaineer-0.7.0.dev5-cp312-cp312-macosx_10_12_x86_64.whl (18.8 MB view details)

Uploaded CPython 3.12 macOS 10.12+ x86-64

mountaineer-0.7.0.dev5-cp311-none-win_amd64.whl (17.3 MB view details)

Uploaded CPython 3.11 Windows x86-64

mountaineer-0.7.0.dev5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (20.7 MB view details)

Uploaded CPython 3.11 manylinux: glibc 2.17+ x86-64

mountaineer-0.7.0.dev5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (20.3 MB view details)

Uploaded CPython 3.11 manylinux: glibc 2.17+ ARM64

mountaineer-0.7.0.dev5-cp311-cp311-macosx_11_0_arm64.whl (17.9 MB view details)

Uploaded CPython 3.11 macOS 11.0+ ARM64

mountaineer-0.7.0.dev5-cp311-cp311-macosx_10_12_x86_64.whl (18.8 MB view details)

Uploaded CPython 3.11 macOS 10.12+ x86-64

mountaineer-0.7.0.dev5-cp310-none-win_amd64.whl (17.3 MB view details)

Uploaded CPython 3.10 Windows x86-64

mountaineer-0.7.0.dev5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (20.7 MB view details)

Uploaded CPython 3.10 manylinux: glibc 2.17+ x86-64

mountaineer-0.7.0.dev5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (20.3 MB view details)

Uploaded CPython 3.10 manylinux: glibc 2.17+ ARM64

mountaineer-0.7.0.dev5-cp310-cp310-macosx_11_0_arm64.whl (17.9 MB view details)

Uploaded CPython 3.10 macOS 11.0+ ARM64

mountaineer-0.7.0.dev5-cp310-cp310-macosx_10_12_x86_64.whl (18.8 MB view details)

Uploaded CPython 3.10 macOS 10.12+ x86-64

File details

Details for the file mountaineer-0.7.0.dev5.tar.gz.

File metadata

  • Download URL: mountaineer-0.7.0.dev5.tar.gz
  • Upload date:
  • Size: 3.3 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/5.1.1 CPython/3.12.7

File hashes

Hashes for mountaineer-0.7.0.dev5.tar.gz
Algorithm Hash digest
SHA256 303f43367132d544f3cc91b4b3775e7ac56eb941dfd715e0e0af92cd7da9a0ae
MD5 9bf03ab7c92eeca1aa477be008862343
BLAKE2b-256 e1de7e3db1fdf4dbaf829e1186b6b394f7a20458c4486bae60ba0f733b5cd0b8

See more details on using hashes here.

File details

Details for the file mountaineer-0.7.0.dev5-cp312-none-win_amd64.whl.

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev5-cp312-none-win_amd64.whl
Algorithm Hash digest
SHA256 767c68b325f5b8729c6a5db42a3972ecf58956b071347dbfc281ec7ece8ee80c
MD5 196a563945a46ae31a0f7b2775043867
BLAKE2b-256 650a9f40d511199c6db974129c2f3d1c54372a0b1101030c06ccbee0d388894c

See more details on using hashes here.

File details

Details for the file mountaineer-0.7.0.dev5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 4bf954235b35142f3023b33384375297a646010883b4c0b2616f53cdad4a4249
MD5 18e096d4b48a00bba59dea7b4508ee6e
BLAKE2b-256 5f62438b02c083405f9a7fc967c5189e2e095c28776a2bcc9ebbd0916330fcc9

See more details on using hashes here.

File details

Details for the file mountaineer-0.7.0.dev5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 e679b2bc15d660a0676cc7713b6076f6a77184f4f10f340be3b332b98e3df171
MD5 8e1959dfb587a47cf02b5c5b9cab30e8
BLAKE2b-256 a2cbaadc824992df2bee9b6f694af2eaa9dcf7e81d48a43f92ddf527d2c97183

See more details on using hashes here.

File details

Details for the file mountaineer-0.7.0.dev5-cp312-cp312-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev5-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 e7665f0f6567127230d0d0ddcd010ef3d896e1a305c9d068f6488ca18aa22f85
MD5 8ce3212ebb2e7f7ba0c21c3fc6b3d6fd
BLAKE2b-256 02117690372db5d90cde06efb8c04ee5c3ad215c20b3114e1360d70386ef3286

See more details on using hashes here.

File details

Details for the file mountaineer-0.7.0.dev5-cp312-cp312-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev5-cp312-cp312-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 507d54654f55746b259137f7b4e36935635679b1c1dd2fc2b5daacf57bcfd60d
MD5 dd0d65055d379d205aeb87ea6a54ce7a
BLAKE2b-256 44d821bad6de6c773bd494689b7ade34b26f3f3a965a40e629dfbed8a435351f

See more details on using hashes here.

File details

Details for the file mountaineer-0.7.0.dev5-cp311-none-win_amd64.whl.

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev5-cp311-none-win_amd64.whl
Algorithm Hash digest
SHA256 e52ba43f69f8d2650da3ce5171af43f48c506e85cb7f58653c01a0f86d78892c
MD5 c4e08c6dd6c3f701ca5ceaa38053d3be
BLAKE2b-256 aba1c8255000d98458dd4122123842d91a0d363140483a4b51e462e872d7ef59

See more details on using hashes here.

File details

Details for the file mountaineer-0.7.0.dev5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 cfc14528abcce70ebb14f0f8bdb8afe29af23453867ab11e1f9dad86fb44df90
MD5 6ff41f363f2b73b66802f14ec4a7cd6b
BLAKE2b-256 4af8cb9a0c96d8598890019c964dc919cf4c65cd0d1df36b08ef8bae8d0a13f1

See more details on using hashes here.

File details

Details for the file mountaineer-0.7.0.dev5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 f17ba1861ef293808588ab7d940d361bc58adac696552df93857807dc1a8a09a
MD5 b17d12c0968f6e9c7643ff4750355220
BLAKE2b-256 9b5a6a0675e1d4df5d5540e3e3848410c3d132da83792049abc7c93d50a7ed25

See more details on using hashes here.

File details

Details for the file mountaineer-0.7.0.dev5-cp311-cp311-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev5-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 a7c10cc713bf9a7c0ab894f0b70e80829eafdf4c06d76244a6c29d1eab162e03
MD5 c2214f019c0d3c58d8501bd9550cc173
BLAKE2b-256 38b22824cb7b5583c37e8cc6ca2fbb438fa9882e105541adba4eb08db5d0004c

See more details on using hashes here.

File details

Details for the file mountaineer-0.7.0.dev5-cp311-cp311-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev5-cp311-cp311-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 71ba40e0303e63f1e320bb990f9b176d8484853c54729b6f35ac2ad56e4c15da
MD5 b6008503cf796c0f43a05bd9b5f331a6
BLAKE2b-256 157ed0c3f2a846710ce04b75ac8e6650f846c9922ee592a70e6029ba16fa7bb3

See more details on using hashes here.

File details

Details for the file mountaineer-0.7.0.dev5-cp310-none-win_amd64.whl.

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev5-cp310-none-win_amd64.whl
Algorithm Hash digest
SHA256 efe401476735ab0f268ea98e2d1f5bd0b05597b73178ccfcba87fc4e1b00cab8
MD5 9cac34db4931ce7bc86aca1d39a4a0a5
BLAKE2b-256 5e6a6e2248910534d3184c116da4a07fc0b3b72e60a0be71ca5b74122f529c36

See more details on using hashes here.

File details

Details for the file mountaineer-0.7.0.dev5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 f9a63d606ec930fe01dd9823ca32bcf4c6dad078503caa16c818099f279cc997
MD5 4f881133ec677eedf9f02c210bb32cd3
BLAKE2b-256 e9ba3f83bb5cdaeeaba0677355fcd8ab13dffb0d73df3158a11106d0c6513e4c

See more details on using hashes here.

File details

Details for the file mountaineer-0.7.0.dev5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 aae3a011cbec9586ea8ba87d371ed8bfc4152dc95e694606ff8a7810c2d776af
MD5 bf805eebb851349d87f0ad8d98f7613d
BLAKE2b-256 45d0651a52052eb643a998ea48092804691166efab49c0516d731093457805ae

See more details on using hashes here.

File details

Details for the file mountaineer-0.7.0.dev5-cp310-cp310-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev5-cp310-cp310-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 da84944c8cf35b706b8e6eb61d9b7c72fbf57bb4435b0dced0e2d598e962f215
MD5 4121126b9457e3ca4950ba8cd77ff46d
BLAKE2b-256 0c7f5098bf5f896ddeafcb25f54e87f670f5bba0e808561d293f81776e91ad31

See more details on using hashes here.

File details

Details for the file mountaineer-0.7.0.dev5-cp310-cp310-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev5-cp310-cp310-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 01716d67ee018377845de016c4d11753b85dd92c4c11684f38618c70c3e89a6a
MD5 84c005e812f562885fcf80fb6add5ea7
BLAKE2b-256 cfadfa127199084d8a86fe37a437e47018b3f99d136e16c590d291cfd89d8a94

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