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

Uploaded Source

Built Distributions

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

Uploaded CPython 3.12 Windows x86-64

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

Uploaded CPython 3.12 macOS 11.0+ ARM64

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

Uploaded CPython 3.11 Windows x86-64

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

Uploaded CPython 3.11 macOS 11.0+ ARM64

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

Uploaded CPython 3.10 Windows x86-64

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

Uploaded CPython 3.10 macOS 11.0+ ARM64

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

File metadata

  • Download URL: mountaineer-0.7.0.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.7.0.dev2.tar.gz
Algorithm Hash digest
SHA256 38fd61f8cced9dcc51af22dd433a72aabed1a71d5a53968da7600e96384f61be
MD5 ce282290e6d41eb874121988860c77b1
BLAKE2b-256 1945ca1347c716fdf952ee80f35709eeb9b2249d31fcfc6dda86fe5d19df0469

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev2-cp312-none-win_amd64.whl
Algorithm Hash digest
SHA256 6b4de2c6e5bdeb2beefec7d28d972b0c369344526ca68dc24702a5def857a9ac
MD5 fb6ee43c71e6f18713344e053f2cf84a
BLAKE2b-256 65095d0702922ae33668bac6562da7c1d9bbec4ff15276d894cab515f723e37c

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 4bf87d6c464420748fba149c0364054bccc79f96c8ac85acf3c88a744f0b6ecd
MD5 cdbf61978a6ba349396106b64dfefd21
BLAKE2b-256 0a093c9c04512b7175853d2295e50573b66fe433a82da99d9da29550771034f8

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 8b10932df5a9fb4e5f204b698ca5634818cbb3698e6689e7b0cdc90152736b74
MD5 d19a823636513d2b7a166607ee44cd89
BLAKE2b-256 9cc7a3784cc4d47761f61fe6ef85b7d7da455e1ec6ff23f5d5f564d2b2a67c44

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev2-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 16775389d2df3d0db5775bf434b4b0a1218c371c036d5976aa55b6ecd6921c53
MD5 1ab53fad0b2a6db667af963f2b9564e9
BLAKE2b-256 4e882c7b1d5d8cd8e36b735e21435cab67ded3d45b629853143515a321f41af5

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev2-cp312-cp312-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 719923472551ad627386b102f077d4927dfc969ef00b36c105ff43d400c9cb1f
MD5 a1399d9aa5160cb05526f2abf000c129
BLAKE2b-256 ec0a28e29da3ae216cc0cc6e6de10b64ba65852eeb20238cf96f552c67a67689

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev2-cp311-none-win_amd64.whl
Algorithm Hash digest
SHA256 9928c0bf44525209306e079bbcceb1893821ab88c56ec5162f9ce9d14f07cf86
MD5 8e34cc464ee2aabb4aa42e8f26e83362
BLAKE2b-256 ba09e92542e264210b0b996fa9ae3684d41c8b68369007c4e6eb925f7aabba89

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 b7275f13f0846b1fcaf29eb36eefe900640b294e0bc523935cf106dd054985f3
MD5 94648560983bba70f67112c1df60d59b
BLAKE2b-256 127b8d37ea6c2e3c8a4f36ae9b6ec666301f9ddd568fc51a94ef09458303dc6a

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 bdca735a3cf45566018016d9997eccec62b070121918ac71886ec4d3fcb322a6
MD5 5d8f2f6d93f85c6f0b79005cfcd6ab50
BLAKE2b-256 0858b80e8347baac34550b78ab797747f00656b2fa945041959e8d6bd566e38b

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev2-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 81eda7709ac74b93f2f74bacd7f1c3ea3e01d80998676ffc20c793f3a8619633
MD5 596a07265d7e3dc065b25bbf28a5c79e
BLAKE2b-256 c772db910d19eece9289f470e13ac8a37db8ace5b80d9ffd8816321ec44091ee

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev2-cp311-cp311-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 4d300634817b9d77e6046722a4f75ac3d6702fa153863cf08e1758db6ffd935b
MD5 7e891bc7c033819375bb9f3764b91e33
BLAKE2b-256 22280f9d0bc94724c24da72c2260f57e605feee4ff1577a0015714223f312508

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev2-cp310-none-win_amd64.whl
Algorithm Hash digest
SHA256 1f44ce87ac20a6097cd28705b4ff3040996d350454aa9352de32d8d5f460e62b
MD5 9874bb75ae4d259226cf81c8fa5086de
BLAKE2b-256 e1e6cf170c9a6f8f201473f7c42be5cdaa5e504128a679d274f86c0f6f5d9f4a

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 e96bbdc356e394a97166817fb33c0dda48b897315ffcfeac2cfba5d198e4f436
MD5 11f318468b87c439f1207995897cc9ca
BLAKE2b-256 e1071f458dd2ff4a576360983c173285a05743cb28432190b42c40cff39db800

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 e8646fcee8db7b6abc7a942c7c23982574efe6fa29ac807906484c2ab7797c4e
MD5 d5d25aa31ace2ef76097739aefb61a90
BLAKE2b-256 effc3fc2073b93691aff7b36d515ec783118f536d149f6d1119577b18616d871

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev2-cp310-cp310-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 95604106f2a46a71ed5a7fe47909981eb623bc506e985d6afaf31b8ae318bfde
MD5 12c226ba3c0c36ab7f69119a25387330
BLAKE2b-256 b01abe6ed51d449a69d305721695d4dbf4560d8952ae11927c7571d40722a1e0

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for mountaineer-0.7.0.dev2-cp310-cp310-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 f12ddbc1db180a9c2982d78e770a84f2dd32278c9a90b66414c2eb60c43fe77b
MD5 cd09af2eb343c1ca7fd18201cdd7052a
BLAKE2b-256 b7b2d1a4c48638fddbc1a4edf08ce3b1f2115f5c04c834f4e87a3e09809feaca

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