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 sqlalchemy.ext.asyncio 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.execute(select(TodoItem))

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

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

Uploaded Source

Built Distributions

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

Uploaded CPython 3.12 Windows x86-64

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

Uploaded CPython 3.12 macOS 11.0+ ARM64

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

Uploaded CPython 3.11 Windows x86-64

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

Uploaded CPython 3.11 macOS 11.0+ ARM64

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

Uploaded CPython 3.10 Windows x86-64

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

Uploaded CPython 3.10 macOS 11.0+ ARM64

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

File metadata

  • Download URL: mountaineer-0.5.2.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.4

File hashes

Hashes for mountaineer-0.5.2.dev1.tar.gz
Algorithm Hash digest
SHA256 5b0f171a05c6823df2d4a32c94000b7ae73df264b246a0eb7d4e8364f7d476f9
MD5 26ff50acc92eb0cfe270ef8f1112cc6e
BLAKE2b-256 486bdaf3e80e244cca1fdef42fd3ba330e756fbe2d8ee29a4aa2e92863430d4b

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev1-cp312-none-win_amd64.whl
Algorithm Hash digest
SHA256 9e972f9f53d46f4e96320c5e3f61f7eed1e696e4d1038b3fcb40fddcf9fdacb7
MD5 45f12640d47e8146ed780d95a0721e19
BLAKE2b-256 2f4b1c736880044574d8996993075c4e4ea2fa697dd4e925fb8cedb7ee17cd1f

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 cd5e9617d75556536fbe5a306d5e57f31b5a33cbc971de21669ca00b0bd0117c
MD5 dccabe24ebdd402fb947caca455d3704
BLAKE2b-256 8bd536369d8b641594b536e3f6552d3ace223b64b22265986de110f1fe47b8fa

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 8aafcd0b5f14eea320fc996be8e41557add40d3ed67dad2716659b93527f3853
MD5 635913c455cbfd9d41497705b8bf183f
BLAKE2b-256 c5904312862eae9d06058cdb55cc9a8208b9113cde6776b234676f1dba96c237

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev1-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 4dd142424d8c9581092e9724ab1a87191ba8044107b171dc8eeac249c807e853
MD5 5e5996067c18a9bfe980ddecf58f8608
BLAKE2b-256 c21e79aea57fb6021bf140682a0b915328299ad5e255b4d82c9941666ef23020

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev1-cp312-cp312-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 f63e0a6be66c0206f9d642ca3c65c0a6a0031f096b0240127954a1cce3eec7a0
MD5 486dff0a86c3f711245ae2fa41919a47
BLAKE2b-256 1c2d275eb1c65ba8b4033586f8dc16bc0f48af60d74c05aa375edfab39142ee9

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev1-cp311-none-win_amd64.whl
Algorithm Hash digest
SHA256 9730f4aed0607e80382f47131d09a82da026fe5c24b559253cc84a3b196357a3
MD5 4a8cfbb3f3d31acd41743958d904c65d
BLAKE2b-256 00dcbae3047e73a325fa16813769895542aa2b1eeb9cce357ad085b52041ceb5

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 562cf30968fcde8b619c6c8abb665da3d10115be7d0cdc84f61720d31dbaa49b
MD5 c15d9c1bea8fc447ef083caf99965964
BLAKE2b-256 6fe7989a4d44370228c92c965be39e3e8551e77d0353422a95f75be8c061777b

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 b33b9230375f157a8ab1af252dfd71075bc6384f445811013626a28d67c6b989
MD5 2f759f8d5ea9fa851ce690195eb145d5
BLAKE2b-256 77b7b32d8be6ef9ba5a20d63b4f3667abf1cedb82a74b53b8eabc50edb362f1a

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev1-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 7bf077b3bc0232af9023c5437a3195c8a9475952fe57b20d251cba0bd4014a4b
MD5 402c23ec2b6ee1d7db679e0eb2a496f4
BLAKE2b-256 49c9f581a6e74051e7e4a83a25fa57cb8ce5d0a6f7906ee77316d7c20a3c1ff6

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev1-cp311-cp311-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 97d97590803851e22ace512ef6d9493f752d54045f8ba2a47c77ba8e92f9a54c
MD5 f15aed54737382344df8bd12b03a9359
BLAKE2b-256 c30bf751f1d91dfac641cea69d7b4eb7cac248503ea10a09d622a5bd761f348c

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev1-cp310-none-win_amd64.whl
Algorithm Hash digest
SHA256 acc428038adf803987ae3f1d830ddec8253f8f56703a85bb21abb3533e28687e
MD5 fa3df82ec455484e32f4fcc23b5e9e10
BLAKE2b-256 2943e736c17f8b637e9496ce3593afca17f384c4b82e0d3fe7216f23b9236386

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 b668b985b00a64177a132853a77ab110a90829d7fb7752ec3f54e52ac0bf2bc2
MD5 1374df3745c7e729669afc1bf491604f
BLAKE2b-256 6152f7d359674b8125b306fc84acadbc1d2ab14e93a510243b08c92193c678a0

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 8caae0687f525142d20b6c60785105eed253abd7ba0f1cf28aa6aa65e5e19ed1
MD5 ddb874d2cd49cd3d58067738002543d6
BLAKE2b-256 55d80bef8dcbdaa401df43b7714f99a686361b4685dea5f239e2ab27feea5585

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev1-cp310-cp310-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 d792f70621a419d60381362486b161bcbca270f4bde2ec579d81200f639b118f
MD5 e347f77a6a815c39dce64ab15405b6b8
BLAKE2b-256 8d31da90a83c78a15e00a2582196d896d2f615430b8fec39105ec147808d60d0

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev1-cp310-cp310-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 24cc2d855a26330ebd46f4b9bc9c30ff9d1fdc0ee264a9fac1f1e6231c76b876
MD5 41b084b7372d48d3cb9bd05ed66a455c
BLAKE2b-256 82e579fbcd85300891d5e9bfa12574cecfa17c45d5219a5300bcc84350755b40

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