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.dev3.tar.gz (3.2 MB view details)

Uploaded Source

Built Distributions

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

Uploaded CPython 3.12 Windows x86-64

mountaineer-0.5.2.dev3-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.dev3-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.dev3-cp312-cp312-macosx_11_0_arm64.whl (17.8 MB view details)

Uploaded CPython 3.12 macOS 11.0+ ARM64

mountaineer-0.5.2.dev3-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.dev3-cp311-none-win_amd64.whl (17.2 MB view details)

Uploaded CPython 3.11 Windows x86-64

mountaineer-0.5.2.dev3-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.dev3-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.dev3-cp311-cp311-macosx_11_0_arm64.whl (17.8 MB view details)

Uploaded CPython 3.11 macOS 11.0+ ARM64

mountaineer-0.5.2.dev3-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.dev3-cp310-none-win_amd64.whl (17.2 MB view details)

Uploaded CPython 3.10 Windows x86-64

mountaineer-0.5.2.dev3-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.dev3-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.dev3-cp310-cp310-macosx_11_0_arm64.whl (17.8 MB view details)

Uploaded CPython 3.10 macOS 11.0+ ARM64

mountaineer-0.5.2.dev3-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.dev3.tar.gz.

File metadata

  • Download URL: mountaineer-0.5.2.dev3.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.dev3.tar.gz
Algorithm Hash digest
SHA256 9f8a974f503721c2fa2ce589bbb4428f52609a9d4509854d1adfa1d5780e4b57
MD5 2e28b5cb047e8573778e10bdccba3b7e
BLAKE2b-256 6a3433b1892c97f0e4d6a2cd8a7739e4485688f631d1b95a001cc419205ca253

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev3-cp312-none-win_amd64.whl
Algorithm Hash digest
SHA256 b579bb22e2783f074e1d4f2f7a3741e59ff3dbf0208f77bc22d7c5979232c607
MD5 af77f548624ce708eb1d981d8f90ccb7
BLAKE2b-256 67b4414bfa15d7db6befcb432aaf24ae9c68141ec5f1bc030a93ca325347033a

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 f798b0c6e509aa0bf18cef369e8e4b25c4fd094b257e9f1c2d10b5441bc6f2ec
MD5 ab5b161de1d25dfd4b412c9926011f9d
BLAKE2b-256 398decdc5c289c15de15935c37f5f2092f895a2a50266716daff4dc22e7f3e13

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 56d8a72e7af89b5fb1e28345f7493483c38735684015f9c264f84c9e9060d4d4
MD5 d517fc74d5ca5a97db2fcf82045d1b22
BLAKE2b-256 f67ab1d0102a558be6e33cfc622722390159fd4c5b9714a2a06a86f7aecf092b

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev3-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 1e91fe6ded48b64f1dc77368e7d2ed1bb4eb17eb0398f8de159aa302c036c8f4
MD5 49f702d950e711dba45d479e5e729621
BLAKE2b-256 754e1d2436646384b212cc3a359c90a0b876ad303bde1cf699cb78a879d64cc0

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev3-cp312-cp312-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 e9529d4d27469905557235f55ea15310cbf933b08ac4b848d40ec962fa825e01
MD5 c6ece3ee3500a42187e75f2471a9a7de
BLAKE2b-256 0fb81ce82e4f42776098750a044acdeeeb9ae650fa7944fdc08b95e97c5d8189

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev3-cp311-none-win_amd64.whl
Algorithm Hash digest
SHA256 7657e4d71d61e5fcfe9bd3c4f8e542e1e71213f7962cc64f1ece7b3054ca13b4
MD5 9ac816883a6c42e2b5a0a2aff912a0ff
BLAKE2b-256 ea3452a9d9c0f6e35e34acbc94f474a8970224615623052d6286fae198fe14f7

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 ae5364e2ef995af89e2c705a82d6b722931d3e6334cddf1ac0c55ff2b670ce4d
MD5 163909c1d082cb9802298a1fb27fefc3
BLAKE2b-256 dd85b709c186ad12626f8457cd44bbfa9024c8332364eeb9cdfce8f2ca2cedca

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 57b33a88e2fae3ac28cca47e7e1d2c5343b12af5e53098ba25572377cf025ff3
MD5 672d2a5ba6621efe3dae5b82bcb2f0e7
BLAKE2b-256 d45f4237719eca209f57b3f13a0faf694dc5da346a9ca73d2f973ccaf77e6827

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev3-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 33c00804c755d03788cb8a8e316ca93b8d65e2ed58ae942d85b2f6685332c84e
MD5 776b1986c816307885f78b9fff114831
BLAKE2b-256 987df086b262bdefe6a594d1af68cc679a60f6c89798c5fe93d0b77304eba002

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev3-cp311-cp311-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 2ab650a2a22825dcee318322c3b64565655031239656e0c23e61c03d3181b852
MD5 c3f214281dcbfb408d6a304a80e7bf3b
BLAKE2b-256 ea820fc080654bcdbd7fb72303bbebae44367a71848fad37a2e52a2f03139d4c

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev3-cp310-none-win_amd64.whl
Algorithm Hash digest
SHA256 f4104ea6f38c55e38c909e0ff7f6b447de096c3b5322a9611cb359a59b04dace
MD5 fa6d0de42f7e898ca60f4a16f67c0ca3
BLAKE2b-256 5ee49bc90ed4e3a4144b87936f3ffc88392f2f9814a404f5bb2079b91fe386c2

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 9fdbbdfdbd92fea842e2f18b7f18dfbd95f99c1a18790186ba8572d2bd961942
MD5 a9c39300d38016eebe68625e2d05fe00
BLAKE2b-256 ef98d05732d709d224936d81a11776fb0f3ec46aeb101e5ee85756d2872bd97d

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 cd72c396f145a281fbb74c60d6e27b015476b5c8d5d0e7977fba0fbf835a0960
MD5 7b19af027fd772067d9510d3794f8a0e
BLAKE2b-256 b0c45a6d277b6e3cec8746c04463f48bd2cc7d6ab0d82538d715d75235a3157e

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev3-cp310-cp310-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 46afd54be9b479901ebd25b093fca60b289982aa8f3b00740ae30ab3cc2549d9
MD5 101dd1e97e1e23c68d219b517eb8ef29
BLAKE2b-256 84279a9100cb3fe7592057e82f103c3c0b5a3850e2a847b3fccc9b7152442d64

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev3-cp310-cp310-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 0ecc056e2fab38c61639c5628b97977bf591e354a9b8480a926b6be23e4af489
MD5 454c3cd805d6f179cf80c7141a90ca0d
BLAKE2b-256 fafb36399a8abf25a23238f7c3beda2ff4ee13c15fae0b176440294653633b54

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