Skip to main content

Yet another nd2 (Nikon NIS Elements) file reader.

Project description

nd2

License PyPI Python Version Test codecov

Yet another .nd2 (Nikon NIS Elements) file reader.

This reader provides a Cython wrapper for the official Nikon SDK – currently version 1.7.0.0, released Jun 23, 2021. (The actual reading of image frames, however, uses a direct memmap approach, instead of the SDK, for performance reasons and to avoid occasional segfaults from the SDK.)

Features good metadata retrieval, and direct to_dask and to_xarray options for lazy and/or annotated arrays.

This library is tested against many nd2 files with the goal of maximizing compatibility and data extraction. (If you find an nd2 file that fails in some way, please open an issue with the file!)

install

pip install nd2

Legacy nd2 (JPEG2000) files are also supported, but require imagecodecs. To install with support for these files use:

pip install nd2[legacy]

usage and API

import nd2
import numpy as np

my_array = nd2.imread('some_file.nd2')                          # read to numpy array
my_array = nd2.imread('some_file.nd2', dask=True)               # read to dask array
my_array = nd2.imread('some_file.nd2', xarray=True)             # read to xarray
my_array = nd2.imread('some_file.nd2', xarray=True, dask=True)  # read file to dask-xarray

# or open a file with nd2.ND2File
f = nd2.ND2File('some_file.nd2')

# attributes:   # example output
f.path          # 'some_file.nd2'
f.shape         # (10, 2, 256, 256)
f.ndim          # 4
f.dtype         # np.dtype('uint16')
f.size          # 1310720  (total voxel elements)
f.sizes         # {'T': 10, 'C': 2, 'Y': 256, 'X': 256}
f.is_rgb        # False (whether the file is rgb)
# if RGB, sizes will have an additional {'c': 3} component

# array output
f.asarray()     # in-memory np.ndarray
np.asarray(f)   # alternative to f.asarray()
f.to_dask()     # delayed dask.array.Array
f.to_xarray()   # in-memory xarray.DataArray, with labeled axes/coords
f.to_xarray(delayed=True)   # delayed xarray.DataArray

                # see below for examples of these structures
# metadata      # returns instance of ...
f.attributes    # nd2.structures.Attributes
f.metadata      # nd2.structures.Metadata
f.experiment    # List[nd2.structures.ExpLoop]
f.text_info     # dict of misc info
f.custom_data   # mishmash of data extracted from file
f.voxel_size()  # VoxelSize(x=0.65, y=0.65, z=1.0)

f.close()       # don't forget to close when done!
f.closed        # boolean, whether the file is closed

# ... or you can use it as a context manager
with nd2.ND2File('some_file.nd2') as ndfile:
    print(ndfile.metadata)
    xarr = ndfile.to_xarray()

Metadata structures

These follow the structure of the nikon SDK outputs. Here are some example outputs

attributes
Attributes(
    bitsPerComponentInMemory=16,
    bitsPerComponentSignificant=16,
    componentCount=2,
    heightPx=32,
    pixelDataType='unsigned',
    sequenceCount=60,
    widthBytes=128,
    widthPx=32,
    compressionLevel=None,
    compressionType=None,
    tileHeightPx=None,
    tileWidthPx=None,
    channelCount=2
)
metadata

Note: the metadata for legacy (JPEG2000) files will be a plain unstructured dict.

Metadata(
    contents=Contents(channelCount=2, frameCount=60),
    channels=[
        Channel(
            channel=ChannelMeta(name='Widefield Green', index=0, colorRGB=65371, emissionLambdaNm=535.0, excitationLambdaNm=None),
            loops=LoopIndices(NETimeLoop=None, TimeLoop=0, XYPosLoop=1, ZStackLoop=2),
            microscope=Microscope(
                objectiveMagnification=10.0,
                objectiveName='Plan Fluor 10x Ph1 DLL',
                objectiveNumericalAperture=0.3,
                zoomMagnification=1.0,
                immersionRefractiveIndex=1.0,
                projectiveMagnification=None,
                pinholeDiameterUm=None,
                modalityFlags=['fluorescence']
            ),
            volume=Volume(
                axesCalibrated=[True, True, True],
                axesCalibration=[0.652452890023035, 0.652452890023035, 1.0],
                axesInterpretation=(
                    <AxisInterpretation.distance: 'distance'>,
                    <AxisInterpretation.distance: 'distance'>,
                    <AxisInterpretation.distance: 'distance'>
                ),
                bitsPerComponentInMemory=16,
                bitsPerComponentSignificant=16,
                cameraTransformationMatrix=[-0.9998932296054086, -0.014612644841559427, 0.014612644841559427, -0.9998932296054086],
                componentCount=1,
                componentDataType='unsigned',
                voxelCount=[32, 32, 5],
                componentMaxima=[0.0],
                componentMinima=[0.0],
                pixelToStageTransformationMatrix=None
            )
        ),
        Channel(
            channel=ChannelMeta(name='Widefield Red', index=1, colorRGB=22015, emissionLambdaNm=620.0, excitationLambdaNm=None),
            loops=LoopIndices(NETimeLoop=None, TimeLoop=0, XYPosLoop=1, ZStackLoop=2),
            microscope=Microscope(
                objectiveMagnification=10.0,
                objectiveName='Plan Fluor 10x Ph1 DLL',
                objectiveNumericalAperture=0.3,
                zoomMagnification=1.0,
                immersionRefractiveIndex=1.0,
                projectiveMagnification=None,
                pinholeDiameterUm=None,
                modalityFlags=['fluorescence']
            ),
            volume=Volume(
                axesCalibrated=[True, True, True],
                axesCalibration=[0.652452890023035, 0.652452890023035, 1.0],
                axesInterpretation=(
                    <AxisInterpretation.distance: 'distance'>,
                    <AxisInterpretation.distance: 'distance'>,
                    <AxisInterpretation.distance: 'distance'>
                ),
                bitsPerComponentInMemory=16,
                bitsPerComponentSignificant=16,
                cameraTransformationMatrix=[-0.9998932296054086, -0.014612644841559427, 0.014612644841559427, -0.9998932296054086],
                componentCount=1,
                componentDataType='unsigned',
                voxelCount=[32, 32, 5],
                componentMaxima=[0.0],
                componentMinima=[0.0],
                pixelToStageTransformationMatrix=None
            )
        )
    ]
)
experiment
[
    TimeLoop(
        count=3,
        nestingLevel=0,
        parameters=TimeLoopParams(
            startMs=0.0,
            periodMs=1.0,
            durationMs=0.0,
            periodDiff=PeriodDiff(avg=16278.339965820312, max=16411.849853515625, min=16144.830078125)
        ),
        type='TimeLoop'
    ),
    XYPosLoop(
        count=4,
        nestingLevel=1,
        parameters=XYPosLoopParams(
            isSettingZ=True,
            points=[
                Position(stagePositionUm=[26950.2, -1801.6000000000001, 498.46000000000004], pfsOffset=None, name=None),
                Position(stagePositionUm=[31452.2, -1801.6000000000001, 670.7], pfsOffset=None, name=None),
                Position(stagePositionUm=[35234.3, 2116.4, 664.08], pfsOffset=None, name=None),
                Position(stagePositionUm=[40642.9, -3585.1000000000004, 555.12], pfsOffset=None, name=None)
            ]
        ),
        type='XYPosLoop'
    ),
    ZStackLoop(count=5, nestingLevel=2, parameters=ZStackLoopParams(homeIndex=2, stepUm=1.0, bottomToTop=True, deviceName='Ti2 ZDrive'), type='ZStackLoop')
]
text_info
{
    'capturing': 'Flash4.0, SN:101412\r\nSample 1:\r\n  Exposure: 100 ms\r\n  Binning: 1x1\r\n  Scan Mode: Fast\r\nSample 2:\r\n  Exposure: 100 ms\r\n  Binning: 1x1\r\n  Scan Mode: Fast',
    'date': '9/28/2021  9:41:27 AM',
    'description': 'Metadata:\r\nDimensions: T(3) x XY(4) x λ(2) x Z(5)\r\nCamera Name: Flash4.0, SN:101412\r\nNumerical Aperture: 0.3\r\nRefractive Index: 1\r\nNumber of Picture Planes: 2\r\nPlane #1:\r\n Name: Widefield Green\r\n Component Count: 1\r\n Modality: Widefield Fluorescence\r\n Camera Settings:   Exposure: 100 ms\r\n  Binning: 1x1\r\n  Scan Mode: Fast\r\n Microscope Settings:   Nikon Ti2, FilterChanger(Turret-Lo): 3 (FITC)\r\n  Nikon Ti2, Shutter(FL-Lo): Open\r\n  Nikon Ti2, Shutter(DIA LED): Closed\r\n  Nikon Ti2, Illuminator(DIA): Off\r\n  Nikon Ti2, Illuminator(DIA) Iris intensity: 3.0\r\n  Analyzer Slider: Extracted\r\n  Analyzer Cube: Extracted\r\n  Condenser: 1 (Shutter)\r\n  PFS, state: On\r\n  PFS, offset: 7959\r\n  PFS, mirror: Inserted\r\n  PFS, Dish Type: Glass\r\n  Zoom: 1.00x\r\n  Sola, Shutter(Sola): Active\r\n  Sola, Illuminator(Sola) Voltage: 100.0\r\nPlane #2:\r\n Name: Widefield Red\r\n Component Count: 1\r\n Modality: Widefield Fluorescence\r\n Camera Settings:   Exposure: 100 ms\r\n  Binning: 1x1\r\n  Scan Mode: Fast\r\n Microscope Settings:   Nikon Ti2, FilterChanger(Turret-Lo): 4 (TRITC)\r\n  Nikon Ti2, Shutter(FL-Lo): Open\r\n  Nikon Ti2, Shutter(DIA LED): Closed\r\n  Nikon Ti2, Illuminator(DIA): Off\r\n  Nikon Ti2, Illuminator(DIA) Iris intensity: 1.5\r\n  Analyzer Slider: Extracted\r\n  Analyzer Cube: Extracted\r\n  Condenser: 1 (Shutter)\r\n  PFS, state: On\r\n  PFS, offset: 7959\r\n  PFS, mirror: Inserted\r\n  PFS, Dish Type: Glass\r\n  Zoom: 1.00x\r\n  Sola, Shutter(Sola): Active\r\n  Sola, Illuminator(Sola) Voltage: 100.0\r\nTime Loop: 3\r\n- Equidistant (Period 1 ms)\r\nZ Stack Loop: 5\r\n- Step: 1 µm\r\n- Device: Ti2 ZDrive',
    'optics': 'Plan Fluor 10x Ph1 DLL'
}
custom_data

No attempt is made to parse this data. It will vary from file to file, but you may find something useful here:

{
    'StreamDataV1_0': {
        'Vector_StreamAnalogIn': '',
        'Vector_StreamDigitalIn': '',
        'Vector_AnalogIn': '',
        'Vector_DigitalIn': '',
        'Vector_Other': '',
        'Vector_StreamAnalogOut': '',
        'Vector_StreamDigitalOut': '',
        'Vector_AnalogOut': '',
        'Vector_DigitalOut': ''
    },
    'NDControlV1_0': {
        'NDControl': {
            'LoopState': {'no_name': [529, 529, 529, 529, 529]},
            'PlayFPS': {'no_name': [20.0, 20.0, 0.0, 20.0, 0.0]},
            'LoopSize': {'no_name': [3, 4, 0, 5, 0]},
            'LoopPosition': {'no_name': [2, 3, 0, 4, 0]},
            'LoopSelection': {'no_name': [b'AAAA', b'AAAAAA==', b'', b'AAAAAAA=', b'']},
            'LoopRangeSelection': {'no_name': [b'AQEB', b'AQEBAQ==', b'', b'AQEBAQE=', b'']},
            'LoopEventSelection': {'no_name': [b'AAAA', b'AAAAAA==', b'', b'AAAAAAA=', b'']},
            'FramesInRange': '',
            'LoopStep': {'no_name': [0, 0, 0, 0, 0]},
            'UserEventType': 2,
            'SelectionStyle': 0,
            'FramesBefore': 2,
            'FramesAfter': 1,
            'TimeBefore': 1.0,
            'TimeAfter': 1.0
        }
    },
    'LUTDataV1_0': {
        'ViewLut': True,
        'LutParam': {
            'Gradient': 0,
            'GradientBrightField': 0,
            'MinSrc': 0,
            'MaxSrc': 65535,
            'GammaSrc': 1.0,
            'MinDst': 0,
            'MaxDst': 65535,
            'ColorSpace': 4,
            'Representation': 0,
            'LutComponentCount': 2,
            'GroupCount': 1,
            'CompLutParam': {
                '00': {'MinSrc': [82, 0.0], 'MaxSrc': [113, 1.0], 'GammaSrc': 1.0, 'MinDst': 0, 'MaxDst': 65535, 'Group': 0},
                '01': {'MinSrc': [82, 0.0], 'MaxSrc': [114, 1.0], 'GammaSrc': 1.0, 'MinDst': 0, 'MaxDst': 65535, 'Group': 0},
                '02': {'MinSrc': [0, 0.0], 'MaxSrc': [65535, 1.0], 'GammaSrc': 1.0, 'MinDst': 0, 'MaxDst': 65535, 'Group': 0}
            },
            'LutDataSpectral': {
                'GainTrueColor': 1.0,
                'OffsetTrueColor': 0.0,
                'GainGrayScale': 1.0,
                'OffsetGrayScal': 0.0,
                'SpectralColorMode': 0,
                'Group00': {
                    'ColorGroup': 16711680,
                    'ColorCustom': 16711680,
                    'GainCustom': 1.0,
                    'OffsetCustom': 0.0,
                    'GainGrouped': 1.0,
                    'OffsetGrouped': 0.0
                }
            }
        },
        'EnableAutoContrast': True,
        'EnableAutoWhite': True,
        'AutoWhiteColor': 16777215,
        'RatioDesc': {
            'Numer': 0,
            'Denom': 1,
            'NumOffset': 0,
            'DenOffset': 0,
            'Min': 0.0,
            'Max': 2.0,
            'BkgndSize': 0,
            'Calibrated': True,
            'Cal.dKd': 224.0,
            'Cal.dVisc': 1.0,
            'Cal.dFmin': 255.0,
            'Cal.dFmax': 1.0,
            'Cal.dRmin': 0.0,
            'Cal.dRmax': 2.0,
            'Cal.dTMeasCalMin': 0.0,
            'Cal.dTMeasCalMax': 0.0,
            'PickFromGraph': True,
            'RatioViewEnabled': True
        },
        'GraphSelected': -1,
        'GraphVerticalSplit': True,
        'GrayGraph': True,
        'ShowAllComp': True,
        'ShowSpectralGraph': True,
        'GraphScale': 0,
        'GraphZoom00': 1.0,
        'GraphOffset00': 0.0,
        'GraphZoom01': 1.0,
        'GraphOffset01': 0.0,
        'GraphZoom02': 1.0,
        'GraphOffset02': 0.0
    },
    'GrabberCameraSettingsV1_0': {
        'GrabberCameraSettings': {
            'CameraUniqueName': 'Hamamatsu C11440-22C SN:101412',
            'CameraUserName': 'Flash4.0, SN:101412',
            'CameraFamilyName': 'ecmC11440_22C',
            'OverloadedUniqueName': '',
            'ModifiedAtJDN': 2459486.07103009,
            'FormatFast': {
                'Desc': {
                    'UniqueName': 'FMT 1x1 16',
                    'Interpretation': 1,
                    'FQModeUsage': 15,
                    'CanExecAsyncSampleGet': True,
                    'Fps': 30.00300030003,
                    'Sensitivity': 1.0,
                    'SensorPixels': {'cx': 2048, 'cy': 2044},
                    'SensorMicrons': {'cx': 13312, 'cy': 13286},
                    'SensorMin': {'cx': 4, 'cy': 4},
                    'SensorStep': {'cx': 2, 'cy': 2},
                    'BinningX': 1.0,
                    'BinningY': 1.0,
                    'SensorSource': {'left': 0, 'top': 0, 'right': 2048, 'bottom': 2044},
                    'FormatText': '16-bit - No Binning',
                    'FormatDesc': '16-bit - No Binning (30.0 FPS)',
                    'CamCorrReq': True,
                    'Comp': 1,
                    'Bpc': 16,
                    'UsageFlags': 1
                },
                'SensorUser': {'left': 512, 'top': 512, 'right': 544, 'bottom': 544}
            },
            'FormatQuality': {
                'Desc': {
                    'UniqueName': 'FMT 1x1 16',
                    'Interpretation': 1,
                    'FQModeUsage': 15,
                    'CanExecAsyncSampleGet': True,
                    'Fps': 30.00300030003,
                    'Sensitivity': 1.0,
                    'SensorPixels': {'cx': 2048, 'cy': 2044},
                    'SensorMicrons': {'cx': 13312, 'cy': 13286},
                    'SensorMin': {'cx': 4, 'cy': 4},
                    'SensorStep': {'cx': 2, 'cy': 2},
                    'BinningX': 1.0,
                    'BinningY': 1.0,
                    'SensorSource': {'left': 0, 'top': 0, 'right': 2048, 'bottom': 2044},
                    'FormatText': '16-bit - No Binning',
                    'FormatDesc': '16-bit - No Binning (30.0 FPS)',
                    'CamCorrReq': True,
                    'Comp': 1,
                    'Bpc': 16,
                    'UsageFlags': 1
                },
                'SensorUser': {'left': 512, 'top': 512, 'right': 544, 'bottom': 544}
            },
            'PropertiesFast': {
                'Exposure': 100.0,
                'LiveSpeedUp': 1,
                'CaptureQuality': 75,
                'CaptureMaxExposure': 10000.0,
                'QuantilRelative': True,
                'QuantilPromile': 0.1,
                'QuantilPixels': 100,
                'EnableAutoExposure': True,
                'ScanMode': 2,
                'Average': 1,
                'Integrate': 1,
                'AverageToQuality': 0.0,
                'AverageCH': '',
                'IntegrateCH': '',
                'AverageToQualityCH': '',
                'IntegrateToQualityCH': '',
                'FlexibleHeight': -1,
                'Negate': 0,
                'MultiExcitation': ''
            },
            'PropertiesFast_Extra': {'PropGroupCount': 0, 'PropGroupUsageArray': {}, 'PropGroupNameArray': {}},
            'PropertiesQuality': {
                'Exposure': 100.0,
                'LiveSpeedUp': 1,
                'CaptureQuality': 75,
                'CaptureMaxExposure': 10000.0,
                'QuantilRelative': True,
                'QuantilPromile': 0.1,
                'QuantilPixels': 100,
                'EnableAutoExposure': True,
                'ScanMode': 2,
                'Average': 1,
                'Integrate': 1,
                'AverageToQuality': 0.0,
                'AverageCH': '',
                'IntegrateCH': '',
                'AverageToQualityCH': '',
                'IntegrateToQualityCH': '',
                'FlexibleHeight': -1,
                'Negate': 0,
                'MultiExcitation': ''
            },
            'PropertiesQuality_Extra': {
                'PropGroupCount': 1,
                'PropGroupUsageArray': {'0': 0},
                'PropGroupNameArray': {'0': 'Use Stored ROI'}
            },
            'Metadata': {
                'Key': 'MV=0,TA=0,CH=1',
                'ChannelCount': 1,
                'Channels': {
                    'Channel_0': {
                        'Color': 22015,
                        'Name': 'Widefield Red',
                        'EmWavelength': 620.0,
                        'ChannelIsActive': True,
                        'ExWavelength': 540.5,
                        'MaxSaturatedValue': 4294967295
                    }
                }
            },
            'LightPath': {
                'TypeID': 0,
                'ExcitationSourceKey': 'LIGHT-EPI',
                'ExcitationSourceName': '',
                'EPIAdditionalFilterKey': '',
                'EPIAdditionalFilterName': '',
                'DIAAdditionalFilterKey': '',
                'DIAAdditionalFilterName': '',
                'LastEmissionFilterKey1': 'Turret-Lo',
                'LastEmissionFilterName1': 'Nikon Ti2, FilterChanger(Turret-Lo)',
                'SetColorManually': True,
                'MultiViewEnabled': True,
                'UpdateLPAutomatically': True
            },
            'ROI': {'Left': 512, 'Top': 512, 'Right': 544, 'Bottom': 544}
        },
        'GrabberCameraSettingsFQMode': 1
    },
    'CustomDataV2_0': {
        'CustomTagDescription_v1.0': {
            'Tag0': {'ID': 'Camera_ExposureTime1', 'Type': 3, 'Group': 2, 'Size': 60, 'Desc': 'Exposure Time', 'Unit': 'ms'},
            'Tag1': {'ID': 'PFS_OFFSET', 'Type': 2, 'Group': 1, 'Size': 60, 'Desc': 'PFS Offset', 'Unit': ''},
            'Tag2': {'ID': 'PFS_STATUS', 'Type': 2, 'Group': 1, 'Size': 60, 'Desc': 'PFS Status', 'Unit': ''},
            'Tag3': {'ID': 'X', 'Type': 3, 'Group': 1, 'Size': 60, 'Desc': 'X Coord', 'Unit': 'µm'},
            'Tag4': {'ID': 'Y', 'Type': 3, 'Group': 1, 'Size': 60, 'Desc': 'Y Coord', 'Unit': 'µm'},
            'Tag5': {'ID': 'Z', 'Type': 3, 'Group': 1, 'Size': 60, 'Desc': 'Z Coord', 'Unit': 'µm'},
            'Tag6': {'ID': 'Z1', 'Type': 3, 'Group': 1, 'Size': 60, 'Desc': 'Ti2 ZDrive', 'Unit': 'µm'}
        }
    },
    'AppInfo_V1_0': {
        'SWNameString': 'NIS-Elements AR',
        'GrabberString': 'Hamamatsu',
        'VersionString': '5.20.02 (Build 1453)',
        'CopyrightString': 'Copyright © 1991-2019  Laboratory Imaging,  http://www.lim.cz',
        'CompanyString': 'NIKON Corporation',
        'NFRString': ''
    },
    'AcqTimeV1_0': 2459486.07044662
}

alternatives

  • pims_nd2 - pims-based reader. ctypes wrapper around the v9.00 (2015) SDK
  • nd2reader - pims-based reader, using reverse-engineered file headers. mostly tested on NIS Elements 4.30.02
  • nd2file - another pure-python, chunk map reader, unmaintained?
  • pyND2SDK - windows-only cython wrapper around the v9.00 (2015) SDK. not on PyPI

The motivating factors for this library were:

  • support for as many nd2 files as possible, with a large test suite
  • pims-independent delayed reader based on dask
  • axis-associated metadata via xarray
  • combined approach of SDK and direct binary reads

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

nd2-0.1.2.tar.gz (9.0 MB view hashes)

Uploaded Source

Built Distributions

nd2-0.1.2-cp39-cp39-win_amd64.whl (962.5 kB view hashes)

Uploaded CPython 3.9 Windows x86-64

nd2-0.1.2-cp39-cp39-manylinux_2_24_x86_64.whl (2.7 MB view hashes)

Uploaded CPython 3.9 manylinux: glibc 2.24+ x86-64

nd2-0.1.2-cp39-cp39-macosx_11_0_arm64.whl (112.7 kB view hashes)

Uploaded CPython 3.9 macOS 11.0+ ARM64

nd2-0.1.2-cp39-cp39-macosx_10_9_x86_64.whl (3.5 MB view hashes)

Uploaded CPython 3.9 macOS 10.9+ x86-64

nd2-0.1.2-cp38-cp38-win_amd64.whl (962.8 kB view hashes)

Uploaded CPython 3.8 Windows x86-64

nd2-0.1.2-cp38-cp38-manylinux_2_24_x86_64.whl (2.7 MB view hashes)

Uploaded CPython 3.8 manylinux: glibc 2.24+ x86-64

nd2-0.1.2-cp38-cp38-macosx_11_0_arm64.whl (112.4 kB view hashes)

Uploaded CPython 3.8 macOS 11.0+ ARM64

nd2-0.1.2-cp38-cp38-macosx_10_9_x86_64.whl (3.5 MB view hashes)

Uploaded CPython 3.8 macOS 10.9+ x86-64

nd2-0.1.2-cp37-cp37m-win_amd64.whl (961.0 kB view hashes)

Uploaded CPython 3.7m Windows x86-64

nd2-0.1.2-cp37-cp37m-manylinux_2_24_x86_64.whl (2.7 MB view hashes)

Uploaded CPython 3.7m manylinux: glibc 2.24+ x86-64

nd2-0.1.2-cp37-cp37m-macosx_10_9_x86_64.whl (3.5 MB view hashes)

Uploaded CPython 3.7m macOS 10.9+ x86-64

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