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

Uploaded Source

Built Distributions

mountaineer-0.7.0.dev1-cp312-none-win_amd64.whl (17.2 MB view details)

Uploaded CPython 3.12 Windows x86-64

mountaineer-0.7.0.dev1-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.dev1-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.dev1-cp312-cp312-macosx_11_0_arm64.whl (17.9 MB view details)

Uploaded CPython 3.12 macOS 11.0+ ARM64

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

Uploaded CPython 3.11 Windows x86-64

mountaineer-0.7.0.dev1-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.dev1-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.dev1-cp311-cp311-macosx_11_0_arm64.whl (17.9 MB view details)

Uploaded CPython 3.11 macOS 11.0+ ARM64

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

Uploaded CPython 3.10 Windows x86-64

mountaineer-0.7.0.dev1-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.dev1-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.dev1-cp310-cp310-macosx_11_0_arm64.whl (17.9 MB view details)

Uploaded CPython 3.10 macOS 11.0+ ARM64

mountaineer-0.7.0.dev1-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.dev1.tar.gz.

File metadata

  • Download URL: mountaineer-0.7.0.dev1.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.7.0.dev1.tar.gz
Algorithm Hash digest
SHA256 c842c5238380d492199ee2dee7fa15011485b3a6fb188fbdf3942851cf5dc4c0
MD5 cc6250fa08bb309a7da6f83f4b6b67df
BLAKE2b-256 8b2a09cab0de5aee986a8ac3b14687ef4b6fba5d9d24276a8ab60762090a558f

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev1-cp312-none-win_amd64.whl
Algorithm Hash digest
SHA256 57880f86d18a31c1a53c8bbf55b22f0a103a6afd66c700d007efd5397fba8fcb
MD5 d8d3721fb0da52908eacf370d6eb70a8
BLAKE2b-256 77bc189f5da775ab9e789b3e328fbc94de08ba9a1213171293e5ec3ffee8ce0f

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 eed00d4636bcce7b86ab0097e927a69b64e2eb4fb09c0a072d0035274d49a767
MD5 3b7691b3ecbc0bf5ffaab38e6e5b85ef
BLAKE2b-256 2f80fc7fd8e3016a9a55518186d7f7de35d8eddef2417eba9f02b2988bc33737

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 50b963907edfdd562ff782cc22f5a99670e2cc7a193be6c6e1722a3d6ee96f4a
MD5 c47cb16809a7a82570fbaa78cee64c17
BLAKE2b-256 030d2e03a14b81898c6a58f1ad2e6121caed93caa2ec4f0a5c3d5b0230e66335

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev1-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 b3ac277de375aa173d5fee165cf92654544af64f27126fe17864804d57ea31e0
MD5 b728257f0065efa4f1d5777d0d150ca6
BLAKE2b-256 516d9f4f2ade848f0177bc50eb08d2e7b16516a2bc484c95e102c3fbc382a5cb

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev1-cp312-cp312-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 6d3a9a9ae8edd156b3ce5344de094aa634d39169cdae5bf61afad9eaa6761e18
MD5 00930110b561f9f41a026faa4e63ccab
BLAKE2b-256 cdc4f7b256226d3b52a12cccdd5f44ec380565a24bdcabb39112de7744abeea4

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev1-cp311-none-win_amd64.whl
Algorithm Hash digest
SHA256 721b39a1a71988a070cdb5fa03550e44d3f0375cf73479de20f1ada69f69dfc4
MD5 b8ddf8a83e49c00265ec8aed9e03a036
BLAKE2b-256 ace54fdeef5b84f709215eb7d2b40556d0fa4925510150a624842a0fb46544ad

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 e677db6c2b1ebf7f606abb27a8d7e7ba9bc28bb6f1c47a6294fad91919560b91
MD5 08b539f2ee7bb4c0226e13f453251d7c
BLAKE2b-256 a38bf8abbcdd38079e3b92dfd43014cd364e04bace748991adc3a5d7a7765fec

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 037038d50fa60fabc9693ebb93a8201cb7cb2c93ac8ca02122cc3e0268405643
MD5 ca120113571560bef91ee478b1c57ff4
BLAKE2b-256 9213455e4602051c277ebfea113278abd448c9424a56ec46b28c8b877e87eb85

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev1-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 f5cf1a1b1138fdf2d879755a205287016f071a969e573331d4af888a0e06612f
MD5 1c212c77a2a1aeecccc0ce92f39efae9
BLAKE2b-256 b9550e8e4e4e338dd5d7a80d48108e8ad8871e9118828731cfe8b13602344892

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev1-cp311-cp311-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 66585392c530a3935635d4478a06456c978ce455ff62863a8e3221cb7ab1ef3f
MD5 8a99c88e248ef383b4f45f83ed94d9f8
BLAKE2b-256 21c0ed26dc08e5eaa82f3113f95b33c52d2b488f51fdc7184883514e36074892

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev1-cp310-none-win_amd64.whl
Algorithm Hash digest
SHA256 3743f2bb9eaea3b6a90512b9f74d9dda3a3cf960bb21f84d0fb96aef69714fd0
MD5 dc526f284a340064893c105bdd772506
BLAKE2b-256 5bc25fc85c0650673d4e71386c7428944bdba5f9dd968e7f21df842c3555db09

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 5b1e0373c6da92e7d4da910635813b63786c295fbd02bc5ab6a551052897d5a6
MD5 1f3a6d18ce805d8c4bad5aba225e7828
BLAKE2b-256 54d59de60b0b7812c222ddf9313e2213c9241e1416c759488657c5e69eda76d1

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 d84376f3acd225606d044f3e6e1af1170660e0b97cc8c1e3cd4649c14d7160aa
MD5 538a8c3b4ea2772916ce64edf482c505
BLAKE2b-256 c19d4a0180626f905b561d05c5365993e57a57f11cc8a3836b94ad1a3f82feb7

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev1-cp310-cp310-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 4746c361814f88768056566b0762ea7aac5688809d637f6423d652f7b6679a14
MD5 f97f0db2039ed46e0bfd88e11ffd2db6
BLAKE2b-256 46eef8bbe4a2b038f5e270d0fd035915809730a434b6827dcc32072a092fa6c5

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev1-cp310-cp310-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 574d601d6de8b53f6838b298ef020bbcac49e57fd6927286e2da6160a369de2f
MD5 1df427085939893a0082cef70c86fd4a
BLAKE2b-256 e8ebdb196a8776c6ad21738419b5240e7b48773f2cc65f367aa83a572db2fb6a

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