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

Uploaded Source

Built Distributions

mountaineer-0.5.2.dev2-cp312-none-win_amd64.whl (17.1 MB view details)

Uploaded CPython 3.12 Windows x86-64

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

Uploaded CPython 3.12 macOS 11.0+ ARM64

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

Uploaded CPython 3.11 Windows x86-64

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

Uploaded CPython 3.11 macOS 11.0+ ARM64

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

Uploaded CPython 3.10 Windows x86-64

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

Uploaded CPython 3.10 macOS 11.0+ ARM64

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

File metadata

  • Download URL: mountaineer-0.5.2.dev2.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.dev2.tar.gz
Algorithm Hash digest
SHA256 51f229cee8fbb028c836e03c8d6dfb932e6ba8418eca9c2e546028ed12ee02ef
MD5 73b421faa2b9da36efcef13148492d4f
BLAKE2b-256 3627cf04353de285355c5b2740b5c9f07a05620555fea1aa6a769b39d70ffac5

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev2-cp312-none-win_amd64.whl
Algorithm Hash digest
SHA256 645b16e8225917d264a46aac54ca33c5fb8b2ad4d1d9e57d4a7b30e9ece1519b
MD5 1e9bae7bb797ec9dbc571d740b5b9580
BLAKE2b-256 188ed3740fe26736a455e3b7e348689eb09ef81e04632d997cda5744aac0a314

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 8c570ebcb3692b9520180bc51af56d387ff97b92444767c846b0b3ba7a852da0
MD5 c3582279a4e68fa02b40167dcba77123
BLAKE2b-256 b0dd46e9ba39583f974cc5be5451f3f19899ea615b4fba9e71586b4eea8d3433

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 1cb3d436aabda0f413d436a3e58357e41c8788c574a4fec1dc57ab65a330d04f
MD5 4656824e2f92fa3ea3514ee59165e7bf
BLAKE2b-256 36609098e23d27a52ad9967757b6701c400bfd246285dc387a3a6258b586bacd

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev2-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 853d45f773c1d23b580ce125ed004b815969922d507acc74e894b448c4ee4aab
MD5 66c8fcc4e824e87f55d3c4c64bbd72d3
BLAKE2b-256 29a846da1bde4432a98dac38a31c686ec7f339cdcac2a3bc06195dd43bb7c77e

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev2-cp312-cp312-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 b1919a3ad33f102f3720a5f726c35eb550544b2eb6b7138f9ed345b95ca048f3
MD5 852c2d783f1d1ac4d55b16876892be2d
BLAKE2b-256 065d7073d63afb25b1c5c706eabab888931b79bf6bc5fb913acf0eb338dff0dd

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev2-cp311-none-win_amd64.whl
Algorithm Hash digest
SHA256 a2152ece3e403d086844c6ad4c0c66f63da9cf4b46d1b00d3daa02471f6e83f1
MD5 2ccf504e664138edca015cf627b7ba1e
BLAKE2b-256 f9343919f666d572ffdd35da1539780d05aa5f34536709a7ff936bc601fdf587

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 a43b5c99a7a06622f9f3b730b60d36ed85854d5c7f63c18fa6f17decaba6f221
MD5 d7f669c7dc07c76a5dfda778ca87c474
BLAKE2b-256 cc4c1bf9a1c29f8e07641d8c0baaa651bc28fe40a1245b45f321ba522d49ad06

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 0a7802379df40bea4077be5bce94b28b5111e0af9f64184150d682cce64e8c16
MD5 0a3ffa79458b8628433be49d202bd8e2
BLAKE2b-256 ab2c2f0340d8e1d33a81dd0e9fc4977f882bd4b0ab77e7a9970e79ebbab3c6f0

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev2-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 f8e57a6515c85bbaec44c336bbe0259f45fcd31918353e67946b2c3d07ae9920
MD5 6772bc825db07514efa5bfa1949a9450
BLAKE2b-256 6a8848d317def1d47e93f82f4536e725f947307c4b4529a7d6ba088cd7a249e8

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev2-cp311-cp311-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 e829f23fb26358eccc54dbf6f6ed34db58031cd65a9217da00c5205790d328b1
MD5 5f658ad2b55d554d4eccff5ab2a1cf86
BLAKE2b-256 f814492b6f22011fe215c4fc84f76ca6aa73474dca105687d572639c2fc698f7

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev2-cp310-none-win_amd64.whl
Algorithm Hash digest
SHA256 253912dc73f99095cbe34207916cdf27015cbcd4f142cdd3b5de398f52939fae
MD5 d6b73605303159716b251a0213ae2306
BLAKE2b-256 6207072d3eb3b28bf5be25bc36b818591649b71b702b19fff769727e147fadbf

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 6e701fb0815d089c3d88517ea87b6f56d4c593d81b6f6549f7ebd5b55b06d0da
MD5 cae0b0d4ca6860845adf4aad65df5c18
BLAKE2b-256 f5abe050a48206c59aebdce62965e83240e9f0db6191ff2dfa13fdde600a05dd

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 e5adc960ce0d82c9852c130702b68b475497279d3c9b0ffb2e678df824afd196
MD5 82329e7ec68b43ba5f6bbcd5dcf8a42e
BLAKE2b-256 8aad479d1ec04b0c40608bc9736cbdf3019c35e4ab9fc9e0def8ab7379543302

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev2-cp310-cp310-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 0a5b09f50f86bd9c7a106d169004ba026990c6006482117788fdcbf98f422e0e
MD5 26d3105c370895da58439eedf07f207f
BLAKE2b-256 d5e67215fd3e30e3c915e0da731afde7aaf5878dccf7eb81612070d124a54603

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.5.2.dev2-cp310-cp310-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 ad968267c1179ed7faee1c8e40bba493fd77abf8d5f31055f70140306e5baa29
MD5 6a4b00d072d362b85c36a98c11b79b4d
BLAKE2b-256 4c51a37ea2e66f6fa6aea2a0ea5bf7b9850ceef72a92f319f2e39418323b5140

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