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

Uploaded Source

Built Distributions

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

Uploaded CPython 3.12 Windows x86-64

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

Uploaded CPython 3.12 macOS 11.0+ ARM64

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

Uploaded CPython 3.11 Windows x86-64

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

Uploaded CPython 3.11 macOS 11.0+ ARM64

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

Uploaded CPython 3.10 Windows x86-64

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

Uploaded CPython 3.10 macOS 11.0+ ARM64

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

File metadata

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

File hashes

Hashes for mountaineer-0.7.0.dev3.tar.gz
Algorithm Hash digest
SHA256 71a34ba4040fb702c77edb74e93655124ade5482d54e3e9d3f0db0d2c2674502
MD5 e027ec9d8fe9ed77be71effcf0f6aedc
BLAKE2b-256 e8a2310b9f64e178ea4e1abb39e5a66034e3d16b7d25ee9e9951bc4c143ea0b4

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev3-cp312-none-win_amd64.whl
Algorithm Hash digest
SHA256 11df0b7daeaf9c4d4cf7d3e686a2834623647952c1e4fe144f08658871e803fd
MD5 4de5c8fbbcb8e0260519bf02aba5c78c
BLAKE2b-256 3da5d81043b02d8e1dc2c9b369223a963844b374b3ae550bf78864b5bae97831

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 44fac45c00d0a5571cfb8f064fa7837ec755e743c71dc19c992794c01e9e69aa
MD5 b13766aa5697331263a1b262149324cf
BLAKE2b-256 c8252f0d94df9707abfba95807ef6bbc445fb7a218671143bc709d17cda8a486

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 3d50d2945e33e46a64d4a127889ea69979dd278fcea86957832d06aa3aa614a7
MD5 f677e546eca912017f59c91148de29fb
BLAKE2b-256 e10c841f3e458955c1d7f6ed9305bd6b987519f4b99b5ed68b04b34227c01e21

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev3-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 6fe9670377402096663dcd2dbd311a6ef90d45754c5690e36cc231a9d1b7f097
MD5 06fa340637000e42e5fcff69a3b6536c
BLAKE2b-256 3262193a4591e7a54abfaad85dc8d9aa49a29b7859b688c2c0e3cf4b2f317d55

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev3-cp312-cp312-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 08156011ca55ac05673ef2d25e91f7101d63b4e57761513704d49ecf7fb0d97f
MD5 329e68bf6ef2de549e1c12864c76d90e
BLAKE2b-256 4a314c2694d363ea27e5ad6b7ac7964a025b67585199c4f7f3a0916be49c8bcd

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev3-cp311-none-win_amd64.whl
Algorithm Hash digest
SHA256 b1f1d2e51db7f0fdcb7036c041900479f1f83e548f79080c0a6bce011d4aa4d5
MD5 d7d136ca7718bb3145bb7bd89e6a5226
BLAKE2b-256 cb6a247da4982803eca9cebddb908d31487a53cfc9e6ebc48c49113c279b42b0

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 28789f2aef5f5276e8dbb0c8512ae0471029bf18cf5033a18423cfbfedd04f27
MD5 8b322b646407ae4f013f228591aa9589
BLAKE2b-256 2bd855397d05d1a339fe0253b7772c897751209b58f3b27cefae4ad4272f003d

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 702980ddf596219a3e2b59930160fa653626ee1b84f97ea7e77f47b99f66b9dd
MD5 dc999841fd2aa364943d6c1f509b9e03
BLAKE2b-256 2ada422f58e91252be6ca4263b390f42d5947878dbd3d5e3ea17e045c6c051d1

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev3-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 9b544b20530c0f677a9de088cacaf1bec6f347e306795d581701829523d228df
MD5 a9cfed3281228aca3be40454408ddf50
BLAKE2b-256 1e52f4aa24e5c527e10611141d6788d94a5539702c5fb15035c260166f7e431a

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev3-cp311-cp311-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 449255786b0574c836da549a08de3cad469a31c9a28ff05c8e530307ff0e6b2d
MD5 134d9cd088f6351de175a30381d3c1a7
BLAKE2b-256 dd937c5d422078b461c47a75bc880b1a08c443477b653a2dd150825801d474e2

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev3-cp310-none-win_amd64.whl
Algorithm Hash digest
SHA256 60a68ce8040a240fe443dece0143e74797dba87c8ddaf0202dd5f2857b1c26d1
MD5 f7424ee6e49f1c4a6f0ceeabad12287e
BLAKE2b-256 c238feedb1291d3002669b6577956a324cb32fbc4e4853834f213115b0579d6f

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 100322fcae5f1c2a9ee1600ea516780cb5885d750437fb6d7594395aac0e7684
MD5 6450f3615c30a7c49b1749d1c97e1312
BLAKE2b-256 cef47f1fb0a5d72f11d1361ae3df2143ed3ac60bf75ffccd3aa135a7144e7b69

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 ac939090f413f0c44c0acc72115439d80bb8b2e9acd830bb82909262f1a9b706
MD5 5d64d4764f7e80a11d499659b7d012f6
BLAKE2b-256 b40bb221cf5f3fc028d8bece36f2e1c9426ced21e6b270462739dc0ee0aae637

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev3-cp310-cp310-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 5bbdaf220818b6300ac5dcf578d91ce9a2b87a0b51524cca6fb8122bda28fdf5
MD5 6818708b3eeefa9feef7901c4b7f0da5
BLAKE2b-256 d3a1175c4c5950de6c1c23ee4c8d035fcbc100cac87234bad66dc5b252277068

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev3-cp310-cp310-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 0a045c2f275b6eb6aa9c68206176fdb01ab6698c5e25a8db5e7f1049f224b92e
MD5 87756ee5f2bb1315649a2409acf787fc
BLAKE2b-256 e8615af9c6e18dac1e0293273a6b1cb4f97fb1dd5df38b908f74ae45717d92eb

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