├── .dialyzer_ignore.exs ├── .formatter.exs ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── secret-to-env.sh └── workflows │ ├── main.yaml │ └── night-dev-reset.yml ├── .gitignore ├── .tool-versions ├── LICENSE.txt ├── Makefile ├── README.md ├── assets ├── bin │ └── figma-to-tailwind.js ├── css │ └── app.css ├── figma │ ├── generated-properties.json │ └── input.json ├── js │ ├── app.js │ ├── chart.js │ ├── datadog.ts │ ├── hooks.js │ ├── socket.js │ └── stripe.js ├── package-lock.json ├── package.json ├── postcss.config.js └── tailwind.config.js ├── cert-setup.sh ├── config ├── config.exs ├── dev.exs ├── prod.exs ├── runtime.exs └── test.exs ├── docker-compose.grafana.yml ├── docker-compose.yml ├── docs ├── HOWTOS.md ├── aws-sso.md ├── clustering.md ├── create-projection.md ├── datadog-app.md ├── domo.md ├── iex-and-observer-in-aws.md ├── multi-tenancy.md ├── snapshot-migration.md └── sync-figma-and-tailwind.md ├── grafana-config.yml ├── lib ├── backend.ex ├── backend │ ├── agent_host_telemetry.ex │ ├── agent_monitor.ex │ ├── agent_monitor │ │ ├── supervisor.ex │ │ ├── telemetry.ex │ │ └── worker.ex │ ├── alarm_handler.ex │ ├── alerting │ │ ├── alert_sender_aws.ex │ │ └── event_handlers.ex │ ├── app.ex │ ├── application.ex │ ├── auth │ │ ├── actor.ex │ │ ├── api_token.ex │ │ ├── auth0.ex │ │ └── command_authorization.ex │ ├── beam_telemetry.ex │ ├── command_translator.ex │ ├── commanded_supervisor.ex │ ├── commanded_telemetry.ex │ ├── crypto │ │ ├── ecto_repo.ex │ │ ├── key.ex │ │ └── secrets_manager_repo.ex │ ├── datadog │ │ └── access_grants.ex │ ├── docs │ │ └── generated │ │ │ ├── _DO_NOT_EDIT.md │ │ │ ├── check.ex │ │ │ └── monitors.ex │ ├── event_store.ex │ ├── event_store_rewriter │ │ ├── middleware.ex │ │ ├── migration.ex │ │ ├── migration_state.ex │ │ ├── migrations │ │ │ └── events_v2.ex │ │ ├── rewrite_task.ex │ │ └── supervisor.ex │ ├── flow_helper.ex │ ├── hubspot │ │ └── event_handlers.ex │ ├── integrations │ │ ├── datadog.ex │ │ ├── feedback.ex │ │ ├── hubspot.ex │ │ ├── slack.ex │ │ └── teams.ex │ ├── json_utils.ex │ ├── log_filter.ex │ ├── metrics.ex │ ├── minute_clock.ex │ ├── mnesia.ex │ ├── monitor_age_telemetry.ex │ ├── monitor_error_telemetry.ex │ ├── monitor_snapshot_telemetry.ex │ ├── notifications │ │ ├── datadog_handler.ex │ │ ├── email_handler.ex │ │ ├── handler.ex │ │ ├── pagerduty_handler.ex │ │ ├── slack_handler.ex │ │ └── webhook_handler.ex │ ├── projections.ex │ ├── projections │ │ ├── account.ex │ │ ├── aggregate │ │ │ ├── api_use_aggregate.ex │ │ │ ├── app_use_aggregate.ex │ │ │ ├── common.ex │ │ │ ├── flow_aggregate.ex │ │ │ ├── new_signup_aggregate.ex │ │ │ └── web_login_aggregate.ex │ │ ├── api_token.ex │ │ ├── dbpa │ │ │ ├── alert.ex │ │ │ ├── alert_delivery.ex │ │ │ ├── analyzer_config.ex │ │ │ ├── check_config.ex │ │ │ ├── instance.ex │ │ │ ├── invite.ex │ │ │ ├── issue.ex │ │ │ ├── issue_event.ex │ │ │ ├── monitor.ex │ │ │ ├── monitor_check.ex │ │ │ ├── monitor_config.ex │ │ │ ├── monitor_error.ex │ │ │ ├── monitor_event.ex │ │ │ ├── monitor_instance.ex │ │ │ ├── monitor_tags.ex │ │ │ ├── monitor_toggled_event.ex │ │ │ ├── monitor_twitter_counts.ex │ │ │ ├── monitor_twitter_info.ex │ │ │ ├── snapshot.ex │ │ │ ├── status_page.ex │ │ │ ├── status_page │ │ │ │ ├── component.ex │ │ │ │ └── subscription.ex │ │ │ ├── subscription.ex │ │ │ ├── subscription_delivery.ex │ │ │ └── visible_monitors.ex │ │ ├── membership.ex │ │ ├── microsoft_teams_command.ex │ │ ├── microsoft_tenant.ex │ │ ├── notice.ex │ │ ├── notice_read.ex │ │ ├── notification_channel │ │ │ └── retry_process.ex │ │ ├── slack_slash_command.ex │ │ ├── slack_workspace.ex │ │ ├── telemetry │ │ │ └── monitor_telemetry.ex │ │ ├── user.ex │ │ └── webhook.ex │ ├── projectors │ │ ├── account_analytics.ex │ │ ├── aggregate.ex │ │ ├── ecto.ex │ │ ├── telemetry.ex │ │ ├── timescale_telemetry.ex │ │ └── type_stream_linker.ex │ ├── prom_ex.ex │ ├── pubsub.ex │ ├── real_time_analytics.ex │ ├── real_time_analytics │ │ ├── HOW_STUFF_WORKS.md │ │ ├── alerting.ex │ │ ├── analysis.ex │ │ ├── event_handlers.ex │ │ ├── loader.ex │ │ ├── mci_process.ex │ │ ├── monitor_events.ex │ │ ├── snapshotting.ex │ │ ├── snapshotting_helpers.ex │ │ ├── status_page_cache.ex │ │ ├── swarm_supervisor.ex │ │ └── teams_body.ex │ ├── release_tasks.ex │ ├── repo.ex │ ├── router.ex │ ├── scheduled_metrics.ex │ ├── send_email.ex │ ├── sentry.ex │ ├── slack │ │ ├── slack_body.ex │ │ ├── slack_commands.ex │ │ ├── slack_helpers │ │ │ ├── slack_channel_helper.ex │ │ │ ├── slack_message_helpers.ex │ │ │ └── slack_subscription_helper.ex │ │ ├── slack_interaction_handler.ex │ │ └── slash_command.ex │ ├── status_page │ │ ├── event_handlers.ex │ │ └── helpers.ex │ ├── statuspages │ │ ├── atlassian_status_page_observer.ex │ │ ├── atlassian_status_page_scraper.ex │ │ ├── aws_status_page_scraper.ex │ │ ├── azure_devops_status_page_scraper.ex │ │ ├── azure_status_page_scraper.ex │ │ ├── gcp_status_page_scraper.ex │ │ ├── generic_status_page_observer.ex │ │ ├── scraper.ex │ │ └── status_page_observer_supervisor.ex │ ├── telemetry.ex │ ├── telemetry │ │ └── telemetry_entry.ex │ ├── telemetry_repo.ex │ ├── telemetry_tolocal.ex │ ├── test_data.ex │ ├── twitter.ex │ ├── twitter │ │ ├── client.ex │ │ ├── supervisor.ex │ │ └── worker.ex │ ├── user_from_auth.ex │ └── utils.ex ├── backend_web.ex ├── backend_web │ ├── api │ │ ├── api_spec.ex │ │ ├── common_parameters.ex │ │ ├── pagination_helpers.ex │ │ ├── render_spec.ex │ │ └── schemas.ex │ ├── api_helpers.ex │ ├── channels │ │ ├── public_realtime_data_socket.ex │ │ └── user_socket.ex │ ├── components │ │ ├── account_monitor_selection.ex │ │ ├── banner.ex │ │ ├── big_number_card.ex │ │ ├── breadcrumb.ex │ │ ├── bulleted_table.ex │ │ ├── doc_popup.ex │ │ ├── download.ex │ │ ├── help_menu.ex │ │ ├── light_dark_mode_toggle.ex │ │ ├── list_group_select.ex │ │ ├── monitor │ │ │ ├── monitor_check.ex │ │ │ ├── monitor_check.html.heex │ │ │ ├── monitor_configure_check.ex │ │ │ ├── monitor_configure_check.html.heex │ │ │ ├── monitor_instance.ex │ │ │ ├── monitor_state_timeline.ex │ │ │ └── twitter_counts.ex │ │ ├── monitor_card.ex │ │ ├── monitor_state_badge.ex │ │ ├── monitor_status.ex │ │ ├── navigation.ex │ │ ├── notice.ex │ │ ├── notice_editor.ex │ │ ├── profile │ │ │ ├── api_token.ex │ │ │ └── email_preferences.ex │ │ ├── profile_menu.ex │ │ ├── safe_image.ex │ │ ├── status_page │ │ │ ├── status_page_component_interface.ex │ │ │ ├── status_page_component_subscription.ex │ │ │ └── ui │ │ │ │ └── page_component.ex │ │ ├── time_line.ex │ │ └── time_line.html.heex │ ├── controllers │ │ ├── agent_controller.ex │ │ ├── auth_controller.ex │ │ ├── command_controller.ex │ │ ├── fallback_controller.ex │ │ ├── health_controller.ex │ │ ├── issues_controller.ex │ │ ├── landing_page_support_controller.ex │ │ ├── monitor_check_controller.ex │ │ ├── monitor_config_controller.ex │ │ ├── monitor_error_controller.ex │ │ ├── monitor_instance_controller.ex │ │ ├── monitor_list_controller.ex │ │ ├── monitor_status_controller.ex │ │ ├── monitor_telemetry_controller.ex │ │ ├── redirect_controller.ex │ │ ├── signup_controller.ex │ │ ├── slack_controller.ex │ │ ├── slack_login_controller.ex │ │ ├── snapshot_controller.ex │ │ ├── status_page_change_controller.ex │ │ ├── subscription_controller.ex │ │ ├── verify_auth_controller.ex │ │ └── webhook_controller.ex │ ├── endpoint.ex │ ├── gettext.ex │ ├── helpers.ex │ ├── i18n.ex │ ├── live │ │ ├── admin │ │ │ ├── accounts_live.ex │ │ │ ├── admin_live.ex │ │ │ ├── metrics_live.ex │ │ │ ├── metrics_live.html.heex │ │ │ └── utilities │ │ │ │ ├── aggregate_view_live.ex │ │ │ │ ├── aggregate_view_live.html.heex │ │ │ │ ├── bulk_monitor_operations.ex │ │ │ │ ├── change_monitor_tag.ex │ │ │ │ ├── free_trial_configure.ex │ │ │ │ ├── invalidate_events.ex │ │ │ │ ├── monitor_config.ex │ │ │ │ ├── monitor_usage_live.ex │ │ │ │ ├── notices_live.ex │ │ │ │ ├── notices_live.html.heex │ │ │ │ ├── rename_check.ex │ │ │ │ ├── rename_monitor.ex │ │ │ │ ├── rta_management.ex │ │ │ │ ├── snapshot_view_live.ex │ │ │ │ ├── snapshot_view_live.html.heex │ │ │ │ └── switch_test_monitor_state.ex │ │ ├── apps_live.ex │ │ ├── apps_live.html.heex │ │ ├── apps_slack_live.ex │ │ ├── apps_slack_live.html.heex │ │ ├── apps_teams_live.ex │ │ ├── apps_teams_live.html.heex │ │ ├── billing_complete_live.ex │ │ ├── billing_live.ex │ │ ├── billing_live.html.heex │ │ ├── datadog │ │ │ ├── auth_complete_live.ex │ │ │ ├── controller_live.ex │ │ │ ├── health_widget_live.ex │ │ │ ├── login_controller.ex │ │ │ ├── start_live.ex │ │ │ ├── start_live.html.heex │ │ │ └── synthetics_wizard_live.ex │ │ ├── demo_monitor_detail_live.ex │ │ ├── demo_monitor_detail_live.html.heex │ │ ├── demo_monitors_live.ex │ │ ├── demo_monitors_live.html.heex │ │ ├── distributions_live.ex │ │ ├── docs_live.ex │ │ ├── init_assigns.ex │ │ ├── login_live.ex │ │ ├── login_live.html.heex │ │ ├── modal_component.ex │ │ ├── monitor_alerting_live.ex │ │ ├── monitor_alerting_live.html.heex │ │ ├── monitor_check_live.ex │ │ ├── monitor_check_live.html.heex │ │ ├── monitor_detail_live.ex │ │ ├── monitor_detail_live.html.heex │ │ ├── monitor_detail_live │ │ │ └── status_page_subscriptions.ex │ │ ├── monitor_detail_live_configure.ex │ │ ├── monitor_detail_live_configure.html.heex │ │ ├── monitor_detail_live_read.ex │ │ ├── monitor_detail_live_read.html.heex │ │ ├── monitor_issues_live.ex │ │ ├── monitor_issues_live.html.heex │ │ ├── monitor_report_live.ex │ │ ├── monitor_report_live.html.heex │ │ ├── monitors_data.ex │ │ ├── monitors_data.html.heex │ │ ├── monitors_errors.ex │ │ ├── monitors_errors.html.heex │ │ ├── monitors_live.ex │ │ ├── monitors_live.html.heex │ │ ├── monitors_subscription_history_live.ex │ │ ├── monitors_subscription_history_live.html.heex │ │ ├── playground_live.ex │ │ ├── playground_live.html.heex │ │ ├── profile_live.ex │ │ ├── signup_live.ex │ │ ├── signup_live.html.heex │ │ ├── slack_login_retry_live.ex │ │ ├── users_live.ex │ │ ├── users_live.html.heex │ │ ├── verify_email_live.ex │ │ └── visible_monitors_live.ex │ ├── plugs │ │ └── caching_body_reader.ex │ ├── router.ex │ ├── telemetry.ex │ ├── templates │ │ ├── layout │ │ │ ├── app.html.heex │ │ │ ├── live.html.heex │ │ │ ├── live_blank.html.heex │ │ │ ├── live_dd.html.heex │ │ │ └── root.html.heex │ │ ├── page │ │ │ └── index.html.eex │ │ └── subscription │ │ │ └── unsubscribe.html.eex │ ├── token.ex │ └── views │ │ ├── error_helpers.ex │ │ ├── error_view.ex │ │ ├── layout_view.ex │ │ ├── login_view.ex │ │ ├── page_view.ex │ │ ├── signup_view.ex │ │ └── subscription_view.ex ├── domain │ ├── account.ex │ ├── account │ │ ├── alert_manager.ex │ │ ├── commands.ex │ │ ├── event_handlers.ex │ │ └── events.ex │ ├── clock.ex │ ├── crypt_repo.ex │ ├── crypt_utils.ex │ ├── datadog_grants.ex │ ├── datadog_grants │ │ ├── commands.ex │ │ └── events.ex │ ├── flow.ex │ ├── flow │ │ ├── commands.ex │ │ ├── events.ex │ │ └── timeout_process.ex │ ├── helpers.ex │ ├── id.ex │ ├── issue │ │ ├── commands.ex │ │ └── events.ex │ ├── issue_manager.ex │ ├── issue_tracker.ex │ ├── middleware │ │ └── type_validation.ex │ ├── monitor.ex │ ├── monitor │ │ ├── commands.ex │ │ ├── event_handlers.ex │ │ └── events.ex │ ├── notice.ex │ ├── notice │ │ ├── commands.ex │ │ └── events.ex │ ├── notification_channel.ex │ ├── notification_channel │ │ ├── commands.ex │ │ ├── events.ex │ │ └── retry_process.ex │ ├── processes │ │ └── slack_integration.ex │ ├── slack_integration.ex │ ├── slack_integration │ │ ├── commands.ex │ │ └── events.ex │ ├── status_page.ex │ ├── status_page │ │ ├── commands.ex │ │ └── events.ex │ ├── tagging.ex │ ├── user.ex │ └── user │ │ ├── commands.ex │ │ ├── event_handlers.ex │ │ └── events.ex ├── mix │ └── tasks │ │ └── metrist │ │ ├── add_event.ex │ │ ├── create_monitor.ex │ │ ├── create_shared_account.ex │ │ ├── create_slack_test_app.ex │ │ ├── dbpa.migrate.ex │ │ ├── dbpa.rollback.ex │ │ ├── empty_visible_monitors.ex │ │ ├── end_event.ex │ │ ├── fix_orphaned_events.ex │ │ ├── generate_manifest_docs.ex │ │ ├── helpers.ex │ │ ├── hubspot_properties.ex │ │ ├── install_monitor.ex │ │ ├── load_gen.ex │ │ ├── make_admin.ex │ │ ├── one_off │ │ ├── README.md │ │ ├── backfill_account_original_user.ex │ │ ├── backfill_aggregate_new_signups.ex │ │ ├── backfill_issue_sources.ex │ │ ├── backfill_status_page_component_states.ex │ │ ├── backfill_user_added_events.ex │ │ ├── clear_status_page_projections.ex │ │ ├── create_additional_monitors_for_gcp_status_page.ex │ │ ├── create_monitors_for_atlassian_status_page.ex │ │ ├── emit_missing_subscription_deleted_events.ex │ │ ├── fix_azure_ad_fncs_monitor.ex │ │ ├── fix_azure_configs.ex │ │ ├── fix_azure_devops_configs.ex │ │ ├── fix_azure_sql_config.ex │ │ ├── fix_customer_configs_after_platform_migration.ex │ │ ├── fix_old_subscription_workspaces.ex │ │ ├── fix_secrets_namespace_in_configs.ex │ │ ├── hubspot_create_and_sync_metrist_properties.ex │ │ ├── hubspot_opt_in_all_beta_users.ex │ │ ├── hubspot_sync_contacts.ex │ │ ├── hubspot_sync_trial_data.ex │ │ ├── migrate_encryption_keys_to_secrets_manager.ex │ │ ├── migrate_monitor_configs.ex │ │ ├── migrate_run_groups_from_rundll.ex │ │ ├── migrate_timeouts_to_ms.ex │ │ ├── monitor_use.ex │ │ ├── optin_missing_hubspot_email_preferences.ex │ │ ├── populate_slack_team_and_user_id_in_users.ex │ │ ├── remove_incorrect_instances.ex │ │ ├── remove_orphaned_slack_workspaces.ex │ │ ├── rename_platform_region_extra_config.ex │ │ ├── reset_dev_status_page.ex │ │ ├── revert_bad_status_page_changes.ex │ │ ├── tag_monitors.ex │ │ └── update_aws_temp_back_to_aws.ex │ │ ├── remove_monitor_config.ex │ │ ├── remove_old_aws_secrets.ex │ │ ├── remove_slack_workspace.ex │ │ ├── remove_status_page.ex │ │ ├── remove_statuspage_component.ex │ │ ├── reporting │ │ └── status_page_scraper.ex │ │ ├── reset_status_page.ex │ │ ├── set_check_name.ex │ │ ├── set_extra_config.ex │ │ ├── set_interval_secs.ex │ │ ├── set_monitor_name.ex │ │ ├── set_monitor_tag.ex │ │ ├── set_read_only.ex │ │ ├── set_run_group.ex │ │ ├── set_run_steps.ex │ │ ├── set_twitter_hashtags.ex │ │ ├── set_visible_monitors.ex │ │ ├── snapshot_migrate.ex │ │ ├── snapshot_migrate_copy.ex │ │ ├── update_monitor_degraded_threshold.ex │ │ ├── update_slack_test_app.ex │ │ └── update_user_account.ex └── signals.ex ├── livebooks ├── .gitignore ├── MET-1246-analysis.livemd ├── MET-1246-doing-the-math.livemd ├── MET-1246-extract-errors.livemd ├── MET-1246-optimize-io.livemd ├── MET-1246-telemetry-correlation.livemd ├── MET-126-data-validation.livemd ├── bayes.livemd ├── met-389-mnesia-telemetry.livemd ├── met-402-account-analytics.livemd └── met-439-the-2500.livemd ├── mix.exs ├── mix.lock ├── priv ├── dbpa_repo │ └── migrations │ │ ├── 20210505210224_add_monitors.exs │ │ ├── 20210506192225_add_instances.exs │ │ ├── 20210512194547_create-invites.exs │ │ ├── 20210517204132_create_snapshots.exs │ │ ├── 20210518203159_create_monitor_checks.exs │ │ ├── 20210518214837_create_monitor_configs.exs │ │ ├── 20210518224502_create_analyzer_configs.exs │ │ ├── 20210519192855_create_monitor_errors.exs │ │ ├── 20210519194035_create_monitor_events.exs │ │ ├── 20210519230516_create_monitor_instances.exs │ │ ├── 20210617203216_create_subscriptions.exs │ │ ├── 20210622154817_add_status_page.exs │ │ ├── 20210623151924_create_monitor_tags.exs │ │ ├── 20210629173209_create_alerts.exs │ │ ├── 20210629173846_create_alert_deliveries.exs │ │ ├── 20210707174251_add_accepted_at_to_invites.exs │ │ ├── 20210722175324_create_subscription_deliveries.exs │ │ ├── 20210722175355_add_display_name_to_subscriptions.exs │ │ ├── 20210726225237_update_subscription_deliveries_indexes.exs │ │ ├── 20210726233551_update_subscription_deliveries_to_denormalize_data.exs │ │ ├── 20210806141833_add_snapshot_correlation_id.exs │ │ ├── 20210901200652_add_run_scenario_fields_to_monitor_config.exs │ │ ├── 20211020144204_create_visible_monitors.exs │ │ ├── 20220110155051_add_analyzer_config_new_defaults.exs │ │ ├── 20220302233844_add_correlation_id_index_to_monitor_events.exs │ │ ├── 20220321232244_add_monitor_events_logical_name_inserted_at_index.exs │ │ ├── 20220324153245_add_monitor_twitter_info.exs │ │ ├── 20220325194248_add_state_to_status_page_changes.exs │ │ ├── 20220509150804_add_blocked_steps_to_monitor_errors.exs │ │ ├── 20220526184853_drop_snapshot_table.exs │ │ ├── 20220701181956_create_monitor_toggled_events.exs │ │ ├── 20220707225725_add_status_page_subscription.exs │ │ ├── 20220715194955_add_status_page_components.exs │ │ ├── 20220715211439_add_maintenance_mode_flag_to_monitor.exs │ │ ├── 20220812212658_remove_exists_from_status_page_components.exs │ │ ├── 20221102202557_add_keyset_pagination_index_for_monitor_errors.exs │ │ ├── 20221104212658_remove_monitor_config_fields.exs │ │ ├── 20221109175355_add_inserted_at_to_subscriptions.exs │ │ ├── 20221115172338_add_monitor_name_to_alerts.exs │ │ ├── 20221201173300_update_monitor_error_index.exs │ │ ├── 20221206141833_add_is_valid.exs │ │ ├── 20230106181040_add_keyset_pagination_index_for_status_page_changes.exs │ │ ├── 20230213191149_add_issue.exs │ │ ├── 20230215172221_add_issue_events.exs │ │ ├── 20230301190504_add_index_to_issue_and_issue_events.exs │ │ ├── 20230322205039_drop_monitors_in_maintenance.exs │ │ ├── 20230322215310_drop_monitor_toggled_events.exs │ │ ├── 20230424141559_add_issues_sources_field.exs │ │ └── 20230424182554_remove_issues_source_field.exs ├── en.json ├── event_store │ └── seeds.exs ├── gettext │ ├── en │ │ └── LC_MESSAGES │ │ │ └── errors.po │ └── errors.pot ├── repo │ ├── migrations │ │ ├── .formatter.exs │ │ ├── 20210504140159_create_users.exs │ │ ├── 20210504142318_create_projection_versions.exs │ │ ├── 20210504143407_add_users_by_email.exs │ │ ├── 20210504201151_add_user_uid.exs │ │ ├── 20210506190919_add_accounts_table.exs │ │ ├── 20210510000950_create_keys.exs │ │ ├── 20210512000356_add_user_is_admin.exs │ │ ├── 20210514140939_add_slack_workspaces.exs │ │ ├── 20210518194512_add_internal_flag_to_accounts.exs │ │ ├── 20210519151754_create_api_tokens.exs │ │ ├── 20210525193500_create_slack_slash_commands.exs │ │ ├── 20210525211719_create_microsoft_teams_commands.exs │ │ ├── 20210526174559_create_microsoft_tenants.exs │ │ ├── 20210527151303_create_teams_workspaces.exs │ │ ├── 20210607183443_drop-teams-workspaces.exs │ │ ├── 20210813174912_create_notices.exs │ │ ├── 20210922224433_add_microsoft_tenant_name.exs │ │ ├── 20210924161702_add_user_hubspot_contact_id.exs │ │ ├── 20211108205221_add_last_login.exs │ │ ├── 20211116201505_add_web_login_aggregate.exs │ │ ├── 20211116201933_add_app_use_aggregate.exs │ │ ├── 20211123005705_add_webhooks.exs │ │ ├── 20211202154419_add_notice_reads.exs │ │ ├── 20220318175952_add_user_timezone.exs │ │ ├── 20220502142526_add_api_counts.exs │ │ ├── 20220506150315_add_flow_aggregate.exs │ │ ├── 20220525164557_add_account_analytics_columns.exs │ │ ├── 20220602001155_add_num_slack_alerts_to_account_analytics_columns.exs │ │ ├── 20220609160333_add_num_slack_commands_to_account.exs │ │ ├── 20220614201813_add_last_webapp_activity_to_account.exs │ │ ├── 20220615172726_add_account_free_trial_end_time.exs │ │ ├── 20220616210321_add_stripe_customer_id.exs │ │ ├── 20220620162152_create_account_memberships.exs │ │ ├── 20220624191342_rename_is_admin_to_is_metrist_admin.exs │ │ ├── 20220704134912_add_user_is_read_only.exs │ │ ├── 20220714180902_add_original_user_id.exs │ │ ├── 20220714181824_add_user_last_seen_slack_team_id.exs │ │ ├── 20220714193749_add_user_last_seen_slack_user_id.exs │ │ ├── 20220912193445_add_new_signups.exs │ │ ├── 20221115214049_add_retry_process.exs │ │ └── 20230328163042_add_datadog_access_grants_table.exs │ └── seeds.exs ├── slack-completion.md ├── static │ ├── favicon.ico │ ├── images │ │ ├── default.png │ │ ├── icon-apps.svg │ │ ├── icon-config.svg │ │ ├── icon-reports.svg │ │ ├── icon-services.svg │ │ ├── phoenix.png │ │ ├── providers │ │ │ ├── aws-logo.svg │ │ │ ├── azure-logo.svg │ │ │ ├── dark-aws-logo.svg │ │ │ ├── dark-azure-logo.svg │ │ │ ├── dark-default-logo.svg │ │ │ ├── dark-gcp-logo.svg │ │ │ ├── default-logo.svg │ │ │ └── gcp-logo.svg │ │ ├── services-illustration.svg │ │ ├── slack-install-choose-monitors.svg │ │ ├── slack-install-subscriptions.svg │ │ └── teams-install.png │ ├── robots.txt │ └── svg │ │ ├── brand │ │ ├── icon.svg │ │ └── logo-combined-white.svg │ │ ├── generic │ │ ├── aws-icon-dark.svg │ │ ├── aws-icon.svg │ │ ├── azure-icon-dark.svg │ │ ├── azure-icon.svg │ │ ├── chevron-down.svg │ │ ├── chevron-up.svg │ │ ├── copy-to-clipboard-icon.svg │ │ ├── datadog-icon.svg │ │ ├── gcp-icon-dark.svg │ │ ├── gcp-icon.svg │ │ ├── icon-book.svg │ │ ├── icon-calendar.svg │ │ ├── icon-check-mark-circle.svg │ │ ├── icon-check-mark.svg │ │ ├── icon-close-circle.svg │ │ ├── icon-close.svg │ │ ├── icon-dark-mode.svg │ │ ├── icon-download.svg │ │ ├── icon-edit-pencil.svg │ │ ├── icon-email.svg │ │ ├── icon-envelope.svg │ │ ├── icon-feedback.svg │ │ ├── icon-filter.svg │ │ ├── icon-gear.svg │ │ ├── icon-info-fill.svg │ │ ├── icon-info.svg │ │ ├── icon-light-mode.svg │ │ ├── icon-plus.svg │ │ ├── icon-profile.svg │ │ ├── icon-solid-selector.svg │ │ ├── icon-teams.svg │ │ ├── icon-trash.svg │ │ ├── icon-webhook.svg │ │ ├── icon-x.svg │ │ ├── logo.svg │ │ ├── ms-teams-icon.svg │ │ ├── password-hide-icon.svg │ │ ├── slack-icon.svg │ │ └── twitter-icon.svg │ │ ├── login │ │ ├── azuread.svg │ │ ├── background-logo.svg │ │ ├── email.svg │ │ ├── github.svg │ │ ├── google.svg │ │ ├── microsoft.svg │ │ ├── signup-message.svg │ │ └── slack.svg │ │ └── monitors │ │ ├── icon-degraded.svg │ │ ├── icon-down.svg │ │ ├── icon-functional-testing.svg │ │ ├── icon-in-app.svg │ │ └── icon-status-component.svg ├── teams-install.md ├── telemetry_repo │ └── migrations │ │ └── .gitkeep ├── telemetry_write_repo │ └── migrations │ │ ├── .gitkeep │ │ ├── 20210518153250_setup_timescale.exs │ │ ├── 20210726200412_create_projection_versions.exs │ │ └── 20211215143920_set_retention.exs └── testdata │ ├── awslambda.json │ └── pagerduty.json ├── rel ├── env.sh.eex ├── overlays │ └── metrist-backend.service └── vm.args.eex ├── slack-dev-manifest-template.json ├── sql ├── dev-grants.sql └── prod-grants.sql ├── test ├── backend │ ├── alerting │ │ └── alert_sender_aws_test.exs │ ├── auth │ │ └── user_authorization_test.exs │ ├── command_translator_test.exs │ ├── crypto │ │ └── secrets_manager_repo_test.exs │ ├── event_store_rewriter │ │ ├── migration_v2_test.exs │ │ └── rewrite_task_test.exs │ ├── json_utils_test.exs │ ├── monitor_age_telemetry_test.exs │ ├── monitor_error_telemetry_test.exs │ ├── notifications │ │ ├── datadog_handler_test.exs │ │ ├── email_hander_test.exs │ │ ├── event_handler_test.exs │ │ ├── pagerduty_handler_test.exs │ │ ├── slack_handler_test.exs │ │ ├── to_pascal_case_helper_test.exs │ │ └── webhook_handler_test.exs │ ├── projections │ │ └── dbpa │ │ │ ├── monitor_config_test.exs │ │ │ └── subscription_test.exs │ ├── projectors │ │ ├── ecto.ex │ │ └── ecto_test.exs │ ├── real_time_analytics │ │ ├── alerting_test.exs │ │ ├── loader_test.exs │ │ ├── monitor_events_test.exs │ │ ├── snapshotting_helpers_test.exs │ │ └── snapshotting_test.exs │ ├── slack │ │ └── slack_body_test.exs │ ├── statuspages │ │ ├── atlassian_status_page_scraper_test.exs │ │ ├── aws_status_page_scraper_test.exs │ │ ├── azure_dev_ops_status_page_scraper_test.exs │ │ ├── azure_status_page_scraper_test.exs │ │ └── gcp_status_page_scraper_test.exs │ ├── test_data │ │ ├── azure_dev_ops_status_page_example.html │ │ ├── azure_status_page_example.html │ │ ├── cloudflare_status_page_example.html │ │ ├── ec2-us-east-1.rss │ │ ├── google_status_page_example.html │ │ ├── trello_status_page_example.html │ │ └── zoom_status_page_example.html │ └── twitter │ │ └── worker_test.exs ├── backend_web │ ├── api_helper_test.exs │ ├── components │ │ ├── list_group_select_test.exs │ │ ├── monitor │ │ │ └── monitor_state_timeline_test.exs │ │ └── status_page │ │ │ └── status_page_component_interface_test.exs │ ├── controllers │ │ ├── agent_controller_test.exs │ │ └── landing_page_support_controller_test.exs │ ├── helpers_test.exs │ └── live │ │ ├── health_widget_live_test.exs │ │ ├── monitor_detail_live_test.exs │ │ ├── monitor_live_test.exs │ │ └── monitors_live_test.exs ├── domain │ ├── account_test.exs │ ├── alert_manager_test.exs │ ├── clock_test.exs │ ├── crypt_utils_test.exs │ ├── flow │ │ └── timeout_process_test.exs │ ├── flow_test.exs │ ├── issue_tracker_test.exs │ ├── monitor_test.exs │ ├── notice_test.exs │ ├── notification_channel │ │ └── retry_process_test.exs │ ├── notification_channel_test.exs │ ├── status_page_test.exs │ ├── tagging_test.exs │ └── user_test.exs ├── mix │ └── tasks │ │ └── metrist │ │ ├── helpers_test.exs │ │ └── install_monitor_test.exs ├── support │ ├── channel_case.ex │ ├── commanded_helpers.ex │ ├── conn_case.ex │ ├── data_case.ex │ ├── notifications │ │ └── test_handlers.ex │ ├── real_time_analytics │ │ └── helpers.ex │ ├── slack │ │ └── helpers.ex │ ├── test_crypt_repo.ex │ └── test_struct.ex └── test_helper.exs └── wordpress ├── README.md ├── realtimedata.css ├── realtimedata.html └── realtimedata.js /.dialyzer_ignore.exs: -------------------------------------------------------------------------------- 1 | # placeholder for dialyzer warning suppression 2 | [] 3 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto, :phoenix, :open_api_spex], 3 | inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | subdirectories: ["priv/*/migrations"] 5 | ] 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | Description here 3 | 4 | ### Jira Item (if applicable) 5 | https://metrist.atlassian.net/jira/software/projects/MET/boards/2?selectedIssue= 6 | 7 | ### Tested (If not tested in dev, explain) 8 | - [ ] local 9 | - [ ] dev 10 | 11 | ### Deployment Steps 12 | - [ ] Are there migrations that have to be run? 13 | - [ ] Do secrets have to be migrated? 14 | - [ ] Is there a specific order that related PR's have to go out? 15 | - [ ] Does a mix task have to be run post deploy? 16 | - [ ] etc. 17 | 18 | ### Related PRs (if any) 19 | https://github.com/Metrist-Software/backend/pull/[number] 20 | -------------------------------------------------------------------------------- /.github/secret-to-env.sh: -------------------------------------------------------------------------------- 1 | for s in $(aws secretsmanager get-secret-value --secret-id /prod/gh-actions/secrets | \ 2 | jq -r '.SecretString | fromjson | to_entries | map(.key + "=" + .value) | .[]'); do 3 | key_val=(${s//=/ }) 4 | echo "::add-mask::${key_val[1]}" 5 | echo "${key_val[0]}=${key_val[1]}" >> $GITHUB_ENV 6 | echo "${key_val[0]}=${key_val[1]}" >> $GITHUB_OUTPUT 7 | done 8 | -------------------------------------------------------------------------------- /.github/workflows/night-dev-reset.yml: -------------------------------------------------------------------------------- 1 | name: Develop nightly reset 2 | 3 | on: 4 | schedule: 5 | - cron: '0 5 * * *' # every day at 5am UTC 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | id-token: write 12 | contents: read 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Configure AWS credentials 16 | uses: aws-actions/configure-aws-credentials@v2 17 | with: 18 | role-to-assume: arn:aws:iam::147803588724:role/github-action 19 | aws-region: us-west-2 20 | - name: Fetch Secret into Env 21 | run: .github/secret-to-env.sh 22 | - uses: actions/checkout@v3 23 | with: 24 | ref: develop #checkout develop 25 | fetch-depth: 0 26 | token: ${{ env.RESET_PAT }} #PAT needed as per https://github.com/ad-m/github-push-action/issues/32 & https://stackoverflow.com/questions/57921401/push-to-origin-from-github-action 27 | - name: Reset develop to main nightly 28 | run: | 29 | git config --global user.email "nobody@metrist.io" 30 | git config --global user.name "Github Action Bot" 31 | git reset --hard origin/main && git commit --allow-empty -m "== Hard reset to main ==" && git push --force 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | /.elixir_ls/ 10 | 11 | # Where 3rd-party dependencies like ExDoc output generated docs. 12 | /doc/ 13 | 14 | # Ignore .fetch files in case you like to edit your project deps locally. 15 | /.fetch 16 | 17 | # If the VM crashes, it generates a dump, let's ignore it too. 18 | erl_crash.dump 19 | 20 | # Also ignore archive artifacts (built via "mix archive.build"). 21 | *.ez 22 | 23 | # Ignore package tarball (built via "mix hex.build"). 24 | backend-*.tar 25 | 26 | # If NPM crashes, it generates a log, let's ignore it too. 27 | npm-debug.log 28 | 29 | # The directory NPM downloads your dependencies sources to. 30 | /assets/node_modules/ 31 | 32 | # Since we are building assets from assets/, 33 | # we ignore priv/static/assets. You may want to comment 34 | # this depending on your deployment strategy. 35 | /priv/static/assets/ 36 | /priv/static/build.txt 37 | .aws-sam 38 | .stackery 39 | priv/plts/ 40 | .vscode 41 | .log/ 42 | localhost+2-key.pem 43 | localhost+2.pem 44 | 45 | *.deb 46 | 47 | Mnesia* 48 | Session.vim 49 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.14.3-otp-25 2 | erlang 25.2.2 3 | nodejs 18.14.0 4 | -------------------------------------------------------------------------------- /assets/figma/generated-properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "color": { 3 | "palette": { 4 | "green-bright": "#6cdaa3ff", 5 | "green-shade": "#3ba88dff", 6 | "blue-bright": "#33d3ffff", 7 | "blue-shade": "#009fd6ff", 8 | "gray-bright": "#9baebcff", 9 | "gray-shade": "#4d6374ff", 10 | "red-bright": "#fd6344ff", 11 | "red-shade": "#af3921ff", 12 | "dark-bright": "#343434ff", 13 | "dark-shade": "#070707ff", 14 | "yellow-bright": "#fce365ff", 15 | "yellow-shade": "#ebc610ff" 16 | }, 17 | "alert": { 18 | "healthy": "#94ca42ff", 19 | "degraded": "#ebc610ff", 20 | "issues": "#ff9900ff", 21 | "down": "#fc4848ff" 22 | }, 23 | "text": { 24 | "secondary": "#b3b3b3ff", 25 | "default": "#343434ff" 26 | }, 27 | "structure": { 28 | "border": "#ddddddff", 29 | "white": "#ffffffff", 30 | "space-indicator": "#eeeeeeff" 31 | } 32 | }, 33 | "gradient": { 34 | "gradient": { 35 | "gradient-green": "linear-gradient(153.2020365463346deg, rgb(108, 218, 163) 0%, rgb(59, 168, 141) 100%)", 36 | "gradient-dark": "linear-gradient(150.24132278708038deg, rgb(52, 52, 52) 0%, rgb(7, 7, 7) 100%)" 37 | } 38 | }, 39 | "effect": { 40 | "color-chip": "0px 4px 16px 0px rgba(0, 0, 0, 0.1)" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /assets/js/stripe.js: -------------------------------------------------------------------------------- 1 | import { loadStripe } from '@stripe/stripe-js'; 2 | 3 | export const InitSetup = { 4 | async mounted() { 5 | const stripe = await loadStripe(this.el.dataset.publicKey) 6 | let elements 7 | this.handleEvent("set_payment_client_secret", ({secret}) => { 8 | const options = { 9 | clientSecret: secret 10 | } 11 | 12 | elements = stripe.elements(options); 13 | const paymentElement = elements.create('payment') 14 | 15 | paymentElement.mount('#payment-element') 16 | paymentElement.on('ready', () => this.pushEvent('payment_ready', 1)) 17 | }) 18 | this.handleEvent("payment_submit", async () => { 19 | const {error} = await stripe.confirmSetup({ 20 | elements, 21 | confirmParams: { 22 | return_url: this.el.dataset.callbackUrl, 23 | } 24 | }); 25 | 26 | if (error) { 27 | this.pushEvent('payment_error', error) 28 | } 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": {}, 3 | "description": " ", 4 | "license": "MIT", 5 | "scripts": { 6 | "deploy": "NODE_ENV=production tailwindcss --postcss --minify -i css/app.css -o ../priv/static/assets/app.css", 7 | "figma-to-tailwind": "./bin/figma-to-tailwind.js" 8 | }, 9 | "dependencies": { 10 | "@datadog/ui-extensions-sdk": "^0.32.1", 11 | "@ryangjchandler/alpine-tooltip": "^1.2.0", 12 | "@stripe/stripe-js": "^1.42.0", 13 | "alpinejs": "^3.10.4", 14 | "apexcharts": "^3.36.0", 15 | "deepmerge": "^4.2.2", 16 | "highlight.js": "^11.6.0", 17 | "phoenix": "file:../deps/phoenix", 18 | "phoenix_html": "file:../deps/phoenix_html", 19 | "phoenix_live_view": "file:../deps/phoenix_live_view", 20 | "phoenix_typed_hook": "file:../deps/phoenix_typed_hook", 21 | "topbar": "^2.0.1" 22 | }, 23 | "devDependencies": { 24 | "@ctrl/tinycolor": "^3.4.1", 25 | "@tailwindcss/typography": "^0.5.2", 26 | "autoprefixer": "^10.4.7", 27 | "eslint": "^8.26.0", 28 | "lodash.kebabcase": "^4.1.1", 29 | "postcss": "^8.4.14", 30 | "style-dictionary": "^3.7.0", 31 | "tailwindcss": "^3.2.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /assets/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-import': {}, 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /cert-setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Having non-SSL for localhost is inconvenient, because HTTP and HTTPS can subtly differ 4 | # plus we have trouble hooking into things like Auth0 5 | # 6 | # Therefore, we download and use `mkcert` to generate certificates and let Phoenix use 7 | # these for dev mode. 8 | # 9 | set -e 10 | 11 | install_mkcert() { 12 | case `uname -o` in 13 | GNU/Linux) 14 | echo "Installing mkcert, expect a sudo prompt..." 15 | cd /tmp 16 | wget https://github.com/FiloSottile/mkcert/releases/download/v1.4.3/mkcert-v1.4.3-linux-amd64 17 | sudo install -m 755 mkcert-v1.4.3-linux-amd64 /usr/local/bin/mkcert 18 | echo "Assuming Debian system, apt installing certutil" 19 | sudo apt install libnss3-tools 20 | ;; 21 | *) 22 | echo "Unknown platform, please fix script" 23 | exit 1 24 | ;; 25 | esac 26 | } 27 | 28 | check_mkcert() { 29 | which mkcert || install_mkcert 30 | } 31 | 32 | check_mkcert 33 | mkcert -install 34 | mkcert localhost 127.0.0.1 ::1 35 | mv *.pem priv 36 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :backend, BackendWeb.Endpoint, 4 | cache_static_manifest: "priv/static/cache_manifest.json" 5 | 6 | config :logger, level: :info 7 | 8 | #config :backend, :metrics_reporting_module, TelemetryMetricsCloudwatch 9 | 10 | # The rest of the configuration is resolved at run-time through Elixir's release scripts 11 | # and can be found in releases.exs 12 | -------------------------------------------------------------------------------- /docker-compose.grafana.yml: -------------------------------------------------------------------------------- 1 | # Separate docker-compose so that grafana metrics won't automatically be collected locally 2 | # Can be started with `docker-compose -f docker-compose.yml -f docker-compose.grafana.yml up` 3 | version: '3' 4 | services: 5 | grafana-agent: 6 | image: grafana/agent 7 | command: -config.expand-env -config.file /etc/agent/agent.yaml 8 | volumes: 9 | - grafana-agent:/var/lib/grafana/data 10 | - ./grafana-config.yml:/etc/grafana/agent.yaml 11 | environment: 12 | - GRAFANA_PROMETHEUS_API_KEY 13 | network_mode: host 14 | volumes: 15 | grafana-agent: 16 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | postgres: 4 | # https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraPostgreSQL.Updates.20180305.html 5 | image: postgres:12.4 6 | volumes: 7 | - postgres:/var/lib/postgresql/data 8 | ports: 9 | - 5432:5432 10 | environment: 11 | - POSTGRES_PASSWORD=postgres 12 | pgadmin: 13 | image: dpage/pgadmin4:6.21 14 | environment: 15 | PGADMIN_DEFAULT_EMAIL: test@metrist.io 16 | PGADMIN_DEFAULT_PASSWORD: 123qwe 17 | PGADMIN_LISTEN_PORT: 80 18 | ports: 19 | - 3002:80 20 | volumes: 21 | - pgadmin:/var/lib/pgadmin 22 | depends_on: 23 | - postgres 24 | tsdb: 25 | image: timescale/timescaledb:2.2.1-pg13 26 | volumes: 27 | - tsdb:/var/lib/postgresql/data 28 | ports: 29 | - 5532:5432 30 | environment: 31 | - POSTGRES_PASSWORD=postgres 32 | volumes: 33 | postgres: 34 | pgadmin: 35 | tsdb: 36 | -------------------------------------------------------------------------------- /docs/HOWTOS.md: -------------------------------------------------------------------------------- 1 | # How-To's 2 | 3 | * [Create a new projection](create-projection.md) 4 | * [Multitenancy](multi-tenancy.md) 5 | * [Sync design system from Figma to Tailwind](sync-figma-and-tailwind.md) 6 | -------------------------------------------------------------------------------- /docs/domo.md: -------------------------------------------------------------------------------- 1 | # Domo 2 | 3 | We use Domo to perform validation on the commands that we dispatch through 4 | Commanded using middleware (see [here](../lib/domain/middleware/type_validation.ex)). 5 | 6 | Domo seems to occasionally have issues with the build cache when the structs it 7 | is used on have changes to them. This can be resolved by running: 8 | 9 | ```elixir 10 | mix clean && mix compile 11 | ``` 12 | 13 | -------------------------------------------------------------------------------- /grafana-config.yml: -------------------------------------------------------------------------------- 1 | prometheus: 2 | wal_directory: /var/lib/grafana/data/wal 3 | global: 4 | scrape_interval: 1m 5 | scrape_timeout: 10s 6 | evaluation_interval: 1m 7 | 8 | configs: 9 | - name: "backend" 10 | scrape_configs: 11 | - job_name: "backend" 12 | static_configs: 13 | - targets: 14 | - "localhost:4000" 15 | remote_write: 16 | - url: https://prometheus-blocks-prod-us-central1.grafana.net/api/prom/push 17 | basic_auth: 18 | username: 170329 19 | password: ${GRAFANA_PROMETHEUS_API_KEY} 20 | -------------------------------------------------------------------------------- /lib/backend.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend do 2 | @moduledoc """ 3 | Backend keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | 10 | @doc """ 11 | Looks up `Application` config 12 | ## Examples 13 | config :backend, :files, [ 14 | uploads_dir: Path.expand("../priv/uploads", __DIR__), 15 | host: [scheme: "http", host: "localhost", port: 4000], 16 | ] 17 | iex> Backend.config([:files, :uploads_dir]) 18 | iex> Bbackend.config([:files, :host, :port]) 19 | """ 20 | def config([main_key | rest] = keyspace, default \\ nil) when is_list(keyspace) do 21 | main = Application.fetch_env!(:backend, main_key) 22 | Enum.reduce_while(rest, main, fn next_key, current -> 23 | case Keyword.fetch(current, next_key) do 24 | {:ok, val} -> {:cont, val} 25 | :error -> {:halt, default} 26 | end 27 | end) 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /lib/backend/agent_host_telemetry.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.AgentHostTelemetry do 2 | @moduledoc """ 3 | PromEx plugin for host telemetry we receive from agents. 4 | """ 5 | use PromEx.Plugin 6 | 7 | @metric_name [:agent, :host_telemetry] 8 | 9 | @impl true 10 | def event_metrics(_opts) do 11 | Event.build( 12 | :agent_host_telemetry, 13 | [ 14 | last_value(@metric_name ++ [:cpu], 15 | tags: [:instance] 16 | ), 17 | last_value(@metric_name ++ [:max_cpu], 18 | tags: [:instance] 19 | ), 20 | last_value(@metric_name ++ [:mem], 21 | tags: [:instance] 22 | ), 23 | last_value(@metric_name ++ [:disk], 24 | tags: [:instance] 25 | ) 26 | ] 27 | ) 28 | end 29 | 30 | def execute(metric_map) do 31 | 32 | :telemetry.execute( 33 | @metric_name, 34 | %{cpu: metric_map.cpu, mem: metric_map.mem, disk: metric_map.disk, max_cpu: metric_map.max_cpu}, 35 | %{instance: metric_map.instance} 36 | ) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/backend/agent_monitor.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.AgentMonitor do 2 | @moduledoc """ 3 | API to agent monitoring subsystem 4 | """ 5 | 6 | @doc """ 7 | Register a heartbeat for the agent instance. This will either start a new monitor 8 | or update an existing one. 9 | """ 10 | defdelegate heartbeat(instance_name), to: Backend.AgentMonitor.Worker 11 | end 12 | -------------------------------------------------------------------------------- /lib/backend/agent_monitor/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.AgentMonitor.Supervisor do 2 | @moduledoc """ 3 | This is the supervisor part of a simple agent monitoring solution. 4 | 5 | If an agent reports in with a Metrist API key (for now), we start 6 | a process that checks whether the agent is alive. An agent is alive 7 | if it reports in at least once every five minutes. If an agent is 8 | found to be dead, a telemetry event is sent so we can alert on it. 9 | 10 | We use Swarm to manage the cluster-wise processes. 11 | """ 12 | 13 | def child_spec(_args) do 14 | %{ 15 | id: __MODULE__, 16 | start: {__MODULE__, :start_link, []}, 17 | type: :supervisor 18 | } 19 | end 20 | 21 | def start_link() do 22 | DynamicSupervisor.start_link(strategy: :one_for_one, name: __MODULE__) 23 | end 24 | 25 | def start_child(instance_name) do 26 | DynamicSupervisor.start_child(__MODULE__, {Backend.AgentMonitor.Worker, [instance_name]}) 27 | end 28 | 29 | def get_monitor(instance_name) do 30 | Swarm.whereis_or_register_name( 31 | worker_name(instance_name), 32 | __MODULE__, 33 | :start_child, 34 | [instance_name] 35 | ) 36 | end 37 | 38 | def worker_name(instance_name), do: {Backend.AgentMonitor, instance_name} 39 | end 40 | -------------------------------------------------------------------------------- /lib/backend/agent_monitor/telemetry.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.AgentMonitor.Telemetry do 2 | @moduledoc """ 3 | Agent monitoring telemetry as a PromEx plugin 4 | """ 5 | use PromEx.Plugin 6 | 7 | @metric_name [:agent, :monitor] 8 | 9 | @impl true 10 | def event_metrics(_opts) do 11 | Event.build( 12 | :agent_monitor_telemetry, 13 | [ 14 | last_value(@metric_name ++ [:is_up], tags: [:instance]) 15 | ] 16 | ) 17 | end 18 | 19 | def mark_up(instance), do: execute(instance, true) 20 | def mark_down(instance), do: execute(instance, false) 21 | 22 | defp execute(instance, is_up?) do 23 | value = if is_up?, do: 1, else: 0 24 | :telemetry.execute( 25 | @metric_name, 26 | %{is_up: value}, 27 | %{instance: instance} 28 | ) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/backend/beam_telemetry.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.Plugins.Beam do 2 | @moduledoc """ 3 | BEAM telemetry in addition to `PromEx.Plugins.Beam` 4 | """ 5 | use PromEx.Plugin 6 | 7 | @metric_prefix [:backend, :beam] 8 | 9 | @impl true 10 | def manual_metrics(_opts) do 11 | [ 12 | beam_system_info() 13 | ] 14 | end 15 | 16 | def beam_system_info do 17 | Manual.build( 18 | :beam_system_info, 19 | {__MODULE__, :execute_beam_system_info, []}, 20 | [ 21 | last_value( 22 | @metric_prefix ++ [:system, :time, :offset], 23 | event_name: @metric_prefix ++ [:system, :time, :offset], 24 | description: "Current time offset between Erlang monotonic time and Erlang system time in native time unit" 25 | ) 26 | ] 27 | ) 28 | end 29 | 30 | def execute_beam_system_info do 31 | :telemetry.execute( 32 | @metric_prefix ++ [:system, :time, :offset], 33 | %{offset: :erlang.time_offset}, 34 | %{} 35 | ) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/backend/crypto/ecto_repo.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.Crypto.EctoRepo do 2 | @doc """ 3 | Repository that stores key information in Ecto. 4 | """ 5 | @behaviour Domain.CryptRepo 6 | import Ecto.Query 7 | 8 | @impl true 9 | def initialize(), do: :ok 10 | 11 | @impl true 12 | def key_for(owner_type, id) do 13 | q = 14 | from k in "keys", 15 | where: 16 | k.owner_type == ^owner_type and 17 | k.owner_id == ^id and 18 | k.is_default == true, 19 | select: {k.id, k.scheme, k.key} 20 | 21 | case Backend.Repo.one(q) do 22 | nil -> create_key_for(owner_type, id) 23 | key -> key 24 | end 25 | end 26 | 27 | @impl true 28 | def get(id) do 29 | Backend.Repo.get(Backend.Crypto.Key, id) 30 | end 31 | 32 | defp create_key_for(owner_type, id) do 33 | scheme = Domain.CryptUtils.current_scheme() 34 | 35 | key = %Backend.Crypto.Key{ 36 | id: Domain.Id.new(), 37 | is_default: true, 38 | owner_id: id, 39 | owner_type: owner_type, 40 | scheme: Atom.to_string(scheme), 41 | key: Domain.CryptUtils.gen_random(Domain.CryptUtils.key_bytes(scheme)) 42 | } 43 | 44 | Backend.Repo.insert(key) 45 | {key.id, key.scheme, key.key} 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/backend/crypto/key.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.Crypto.Key do 2 | use Ecto.Schema 3 | 4 | @primary_key {:id, :string, []} 5 | schema "keys" do 6 | field :is_default, :boolean, default: false 7 | field :key, :string, redact: true 8 | field :key_id, :string 9 | field :owner_id, :string 10 | field :owner_type, :string 11 | field :scheme, :string 12 | 13 | timestamps() 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/backend/datadog/access_grants.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.Datadog.AccessGrants do 2 | use Ecto.Schema 3 | import Ecto.Query 4 | 5 | @primary_key {:id, :string, []} 6 | schema "datadog_access_grants" do 7 | field :verifier, :string 8 | field :user_id, :string 9 | field :access_token, :string 10 | field :refresh_token, :string 11 | field :scope, {:array, :string} 12 | field :expires_in, :integer 13 | field :expires_at, :utc_datetime 14 | 15 | timestamps() 16 | end 17 | 18 | def get_by_user_id(user_id) do 19 | __MODULE__ 20 | |> where(user_id: ^user_id) 21 | |> Backend.Repo.one() 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/backend/docs/generated/_DO_NOT_EDIT.md: -------------------------------------------------------------------------------- 1 | # Generated Doc Outputs 2 | 3 | All modules in this folder are automatically generated and should not be manually 4 | edited. Any changes made here are highly likely to be overwritten the next time 5 | the generation task is run. 6 | 7 | Data in these modules are sourced from the manifests found in the [monitors](https://github.com/Metrist-Software/monitors) 8 | repo and built using the [metrist.generate_manifest_docs](../../../mix//tasks//metrist/generate_manifest_docs.ex) 9 | mix task 10 | -------------------------------------------------------------------------------- /lib/backend/event_store.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.EventStore do 2 | use EventStore, 3 | otp_app: :backend, 4 | serializer: Commanded.Serialization.JsonSerializer, 5 | column_data_type: "jsonb" 6 | end 7 | 8 | defmodule Backend.EventStore.Migration do 9 | use EventStore, 10 | otp_app: :backend, 11 | serializer: Commanded.Serialization.JsonSerializer, 12 | column_data_type: "jsonb" 13 | end 14 | -------------------------------------------------------------------------------- /lib/backend/event_store_rewriter/middleware.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.EventStoreRewriter.Middleware do 2 | @behaviour Commanded.Middleware 3 | 4 | alias Commanded.Middleware.Pipeline 5 | 6 | def before_dispatch(pipeline) do 7 | case Backend.EventStoreRewriter.RewriteTask.get_status() do 8 | :finalizing -> Pipeline.halt(pipeline) 9 | _ -> pipeline 10 | end 11 | end 12 | 13 | def after_dispatch(pipeline), do: pipeline 14 | def after_failure(pipeline), do: pipeline 15 | end 16 | -------------------------------------------------------------------------------- /lib/backend/log_filter.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.LogFilter do 2 | @moduledoc """ 3 | Simple module to filter out some logger messages 4 | """ 5 | 6 | # Only pattern match on :warning level messages so this doesn't end up being too expensive. 7 | # Everything else will just pass through with :ignore 8 | # Have to use a log filter here as filtering out all warn messages from commanded_eventstore 9 | # or all logging entirely from commanded_eventstore woudln't be what we want. 10 | defp ignore_link_duplicate_event_filter(%{level: :warning, msg: {:string, msg}}, _) do 11 | if :unicode.characters_to_binary(msg) == "Failed to link events to stream due to: :duplicate_event" do 12 | :stop 13 | else 14 | :ignore 15 | end 16 | end 17 | defp ignore_link_duplicate_event_filter(_, _), do: :ignore 18 | 19 | def setup_filters() do 20 | :logger.add_primary_filter(:ignore_link_duplicate_event, {&ignore_link_duplicate_event_filter/2, []}) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/backend/projections/aggregate/common.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.Projections.Aggregate.Common do 2 | @moduledoc """ 3 | Some commonly-used code to support our metrics collection ("aggregate") 4 | code. 5 | """ 6 | 7 | @doc """ 8 | Return the NaiveDateTime that represents the cut-off time for the indicated 9 | period. 10 | """ 11 | def since(amount, period), do: Timex.shift(NaiveDateTime.utc_now(), [{period, -amount}]) 12 | 13 | import Ecto.Query 14 | 15 | @doc """ 16 | Cleanup old records. 17 | """ 18 | def cleanup(module_name) do 19 | cleanup_date = since(1, :months) 20 | # we only need up to 1 month back, rest is in the event store if we need it 21 | module_name 22 | |> where([a], a.time <= ^cleanup_date) 23 | |> Backend.Repo.delete_all() 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/backend/projections/aggregate/new_signup_aggregate.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.Projections.Aggregate.NewSignupAggregate do 2 | use Ecto.Schema 3 | 4 | @primary_key false 5 | schema "aggregate_new_signups" do 6 | field :id, :string 7 | field :time, :naive_datetime_usec 8 | end 9 | 10 | import Ecto.Query 11 | alias Backend.Repo 12 | alias Backend.Projections.Aggregate.Common 13 | 14 | def new_signups(:daily), do: do_active_count(:days) 15 | def new_signups(:weekly), do: do_active_count(:weeks) 16 | def new_signups(:monthly), do: do_active_count(:months) 17 | 18 | defp do_active_count(period) do 19 | since = Common.since(1, period) 20 | __MODULE__ 21 | |> select([a], count(a.id, :distinct)) 22 | |> where([a], a.time >= ^since) 23 | |> Repo.one() 24 | end 25 | 26 | def cleanup() do 27 | Common.cleanup(__MODULE__) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/backend/projections/aggregate/web_login_aggregate.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.Projections.Aggregate.WebLoginAggregate do 2 | use Ecto.Schema 3 | 4 | @primary_key false 5 | schema "aggregate_web_login" do 6 | field :id, :string 7 | field :time, :naive_datetime_usec 8 | field :user_id, :string 9 | field :is_internal, :boolean 10 | end 11 | 12 | import Ecto.Query 13 | alias Backend.Repo 14 | alias Backend.Projections.Aggregate.Common 15 | 16 | def active_web_users(:daily), do: do_active_count(:days) 17 | def active_web_users(:weekly), do: do_active_count(:weeks) 18 | def active_web_users(:monthly), do: do_active_count(:months) 19 | 20 | defp do_active_count(period) do 21 | since = Common.since(1, period) 22 | 23 | __MODULE__ 24 | |> select([a], count(a.user_id, :distinct)) 25 | |> where([a], a.time >= ^since) 26 | |> where([a], a.is_internal == false) 27 | |> Repo.one() 28 | end 29 | 30 | def cleanup() do 31 | Common.cleanup(__MODULE__) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/backend/projections/api_token.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.Projections.APIToken do 2 | use Ecto.Schema 3 | 4 | @primary_key {:api_token, :string, []} 5 | schema "api_tokens" do 6 | field :account_id, :string 7 | 8 | timestamps() 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/backend/projections/dbpa/alert.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.Projections.Dbpa.Alert do 2 | use Ecto.Schema 3 | 4 | @primary_key {:id, :string, []} 5 | schema "alerts" do 6 | field :monitor_logical_name, :string 7 | field :state, Ecto.Enum, values: Backend.Projections.Dbpa.Snapshot.states() 8 | field :is_instance_specific, :boolean 9 | field :subscription_id, :string 10 | field :formatted_messages, :map 11 | field :affected_checks, {:array, :map} 12 | field :affected_regions, {:array, :string} 13 | field :generated_at, :naive_datetime_usec 14 | field :correlation_id, :string 15 | field :monitor_name, :string 16 | 17 | timestamps() 18 | end 19 | 20 | import Ecto.Query 21 | alias Backend.Repo 22 | 23 | def alert_count() do 24 | Backend.Projections.list_accounts(type: :external) 25 | |> Enum.map(fn acct -> 26 | Backend.Repo.aggregate(__MODULE__, :count, :id, prefix: Backend.Repo.schema_name(acct)) 27 | end) 28 | |> Enum.sum() 29 | end 30 | 31 | def get_alert_by_id(account_id, alert_id) do 32 | (from e in __MODULE__, 33 | where: e.id == ^alert_id) 34 | |> put_query_prefix(Repo.schema_name(account_id)) 35 | |> Repo.one() 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/backend/projections/dbpa/alert_delivery.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.Projections.Dbpa.AlertDelivery do 2 | use Ecto.Schema 3 | 4 | @primary_key {:id, :string, []} 5 | schema "alert_deliveries" do 6 | field :alert_id, :string 7 | field :delivery_method, :string 8 | field :delivered_by_region, :string 9 | field :started_at, :naive_datetime_usec 10 | field :completed_at, :naive_datetime_usec 11 | 12 | timestamps() 13 | end 14 | 15 | def alert_delivery_count() do 16 | Backend.Projections.list_accounts(type: :external) 17 | |> Enum.map(fn acct -> 18 | Backend.Repo.aggregate(__MODULE__, :count, :id, prefix: Backend.Repo.schema_name(acct)) 19 | end) 20 | |> Enum.sum() 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/backend/projections/dbpa/instance.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.Projections.Dbpa.Instance do 2 | use Ecto.Schema 3 | import Ecto.Query 4 | alias Backend.Repo 5 | 6 | @primary_key {:name, :string, []} 7 | schema "instances" do 8 | 9 | timestamps() 10 | end 11 | 12 | def get_instances(account_id) do 13 | (from i in __MODULE__) 14 | |> put_query_prefix(Repo.schema_name(account_id)) 15 | |> Repo.all() 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/backend/projections/dbpa/monitor_toggled_event.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.Projections.Dbpa.MonitorToggledEvent do 2 | use Ecto.Schema 3 | 4 | @primary_key {:id, :string, []} 5 | schema "monitor_toggled_events" do 6 | field :monitor_logical_name, :string 7 | field :state, Ecto.Enum, values: Backend.Projections.Dbpa.Snapshot.states() 8 | 9 | timestamps() 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/backend/projections/dbpa/visible_monitors.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.Projections.Dbpa.VisibleMonitor do 2 | use Ecto.Schema 3 | 4 | @primary_key false 5 | schema "visible_monitors" do 6 | field :monitor_logical_name, :string 7 | end 8 | 9 | def visible_monitor_logical_names(acct) do 10 | __MODULE__ 11 | |> Backend.Repo.all(prefix: Backend.Repo.schema_name(acct)) 12 | |> Enum.map(& &1.monitor_logical_name) 13 | end 14 | 15 | def default_visible_monitor_logical_names() do 16 | [] 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/backend/projections/microsoft_teams_command.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.Projections.MicrosoftTeamsCommand do 2 | use Ecto.Schema 3 | 4 | @primary_key {:id, :string, []} 5 | schema "microsoft_teams_commands" do 6 | field :data, :map 7 | 8 | timestamps() 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/backend/projections/microsoft_tenant.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.Projections.MicrosoftTenant do 2 | use Ecto.Schema 3 | 4 | @primary_key {:id, :string, []} 5 | schema "microsoft_tenants" do 6 | field :account_id, :string 7 | field :name, :string 8 | field :team_id, :string 9 | field :team_name, :string 10 | field :service_url, :string 11 | end 12 | 13 | import Ecto.Query 14 | alias Backend.Repo 15 | 16 | def get_microsoft_tenants(account_id) do 17 | from(mt in __MODULE__, where: mt.account_id == ^account_id) 18 | |> Repo.all() 19 | end 20 | 21 | def has_microsoft_tenants?(account_id) do 22 | from(mt in __MODULE__, where: mt.account_id == ^account_id) 23 | |> Repo.exists?() 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/backend/projections/notice_read.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.Projections.NoticeRead do 2 | use Ecto.Schema 3 | 4 | @primary_key {:id, :string, []} 5 | schema "notice_reads" do 6 | belongs_to :user, Backend.Projections.User, type: :string 7 | belongs_to :notice, Backend.Projections.Notice, type: :string 8 | 9 | timestamps() 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/backend/projections/notification_channel/retry_process.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.Projections.NotificationChannel.RetryProcess do 2 | use Ecto.Schema 3 | 4 | @primary_key {:id, :string, []} 5 | schema "notification_retry_process" do 6 | timestamps() 7 | end 8 | 9 | import Ecto.Query 10 | alias Backend.Repo 11 | 12 | def register(id) do 13 | %__MODULE__{id: id} 14 | |> Repo.insert(on_conflict: :nothing) 15 | end 16 | 17 | def deregister(id) do 18 | (from p in __MODULE__, where: p.id == ^id) 19 | |> Repo.delete_all() 20 | end 21 | 22 | def all_ids do 23 | (from p in __MODULE__, select: p.id) 24 | |> Repo.all() 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/backend/projections/slack_slash_command.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.Projections.SlackSlashCommand do 2 | use Ecto.Schema 3 | 4 | @primary_key {:id, :string, []} 5 | schema "slack_slash_commands" do 6 | field :data, :map 7 | 8 | timestamps() 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/backend/real_time_analytics.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.RealTimeAnalytics do 2 | @moduledoc """ 3 | Interface to RTA for non-RTA modules. This is considered the "public API" for 4 | real-time analytics. 5 | """ 6 | 7 | defdelegate get_snapshot(account_id, monitor_logical_name), 8 | to: Backend.RealTimeAnalytics.Analysis 9 | 10 | def get_snapshot_or_nil(account_id, monitor_logical_name) do 11 | case get_snapshot(account_id, monitor_logical_name) do 12 | {:ok, snapshot} -> snapshot 13 | _ -> nil 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/backend/scheduled_metrics.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.ScheduledMetrics do 2 | use GenServer 3 | 4 | require Logger 5 | 6 | def start_link(_) do 7 | GenServer.start_link(__MODULE__, nil, name: ScheduledMetrics) 8 | end 9 | 10 | def get(), do: GenServer.call(ScheduledMetrics, :get) 11 | 12 | @impl true 13 | def init(_args) do 14 | send(self(), :work) 15 | 16 | {:ok, %Backend.Metrics{}} 17 | end 18 | 19 | @impl true 20 | def handle_call(:get, _from, current_metrics), do: {:reply, current_metrics, current_metrics} 21 | 22 | @impl true 23 | def handle_info(:work, current_metrics) do 24 | Logger.info("Updating metrics") 25 | 26 | metrics = try do 27 | Backend.Metrics.fetch() 28 | rescue 29 | error -> 30 | Logger.warn("Could not update metrics, error is #{inspect error}.") 31 | current_metrics 32 | end 33 | 34 | try do 35 | Backend.Metrics.cleanup() 36 | rescue 37 | error -> 38 | Logger.warn("Could not cleanup metrics, error is #{inspect error}.") 39 | end 40 | 41 | Logger.info("Finished updating metrics") 42 | 43 | schedule_work() 44 | {:noreply, metrics} 45 | end 46 | 47 | defp schedule_work() do 48 | Process.send_after(self(), :work, :timer.minutes(30)) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/backend/sentry.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.Sentry do 2 | def before_send(event) do 3 | reg = ~r/.*received unexpected message: {:(EXIT|DOWN),.*}.*/ 4 | 5 | if !Regex.match?(reg, event.message) do 6 | event 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/backend/slack/slack_helpers/slack_channel_helper.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.Slack.SlackHelpers.SlackChannelHelper do 2 | 3 | require Logger 4 | 5 | @channel_name_pattern ~r/<#(?(?i)c.+)\|(?.*)>/ 6 | 7 | def is_channel_valid?(_channel = nil) do 8 | false 9 | end 10 | 11 | def is_channel_valid?(channel) do 12 | String.match?(channel, @channel_name_pattern) 13 | end 14 | 15 | # "deconstructs" escaped form into tuple {C1234, #general} 16 | def get_channel(id_and_name) do 17 | map = Regex.named_captures(@channel_name_pattern, id_and_name) 18 | id = map 19 | |> Map.get("channel_id") 20 | |> String.upcase() 21 | map = Map.replace(map, "channel_id", id) 22 | Map.get(map, "channel_name") 23 | |> maybe_add_channel_name(map) 24 | end 25 | 26 | defp maybe_add_channel_name(_channel_name = "", map) do 27 | { Map.get(map, "channel_id"), nil } # for private channels 28 | end 29 | 30 | defp maybe_add_channel_name(channel_name, map) do 31 | { Map.get(map, "channel_id"), "#" <> channel_name } 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /lib/backend/slack/slash_command.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.Slack.SlashCommand do 2 | 3 | use TypedStruct 4 | 5 | typedstruct enforce: true do 6 | field :token, String.t() 7 | field :team_id, String.t() 8 | field :team_domain, String.t() 9 | field :enterprise_id, String.t() 10 | field :enterprise_name, String.t() 11 | field :channel_id, String.t() 12 | field :channel_name, String.t() 13 | field :user_id, String.t() 14 | field :username, String.t() 15 | field :command, String.t() 16 | field :text, String.t() 17 | field :response_url, String.t() 18 | field :trigger_id, String.t() 19 | field :account_id, String.t() 20 | end 21 | 22 | end 23 | -------------------------------------------------------------------------------- /lib/backend/statuspages/scraper.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.StatusPages.Scraper do 2 | @callback name() :: String.t 3 | @callback scrape() :: {:ok, [{String.t, [%Domain.StatusPage.Commands.Observation{}]}]} | {:error, String.t} 4 | end 5 | -------------------------------------------------------------------------------- /lib/backend/telemetry/telemetry_entry.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.Telemetry.TelemetryEntry do 2 | use Ecto.Schema 3 | @primary_key false 4 | import Ecto.Changeset 5 | 6 | schema "monitor_telemetry" do 7 | field :account_id, :string 8 | field :check_id, :string 9 | field :instance_id, :string 10 | field :monitor_id, :string 11 | field :time, :utc_datetime_usec 12 | field :value, :float 13 | end 14 | 15 | @doc false 16 | def changeset(telemetry_entry, attrs) do 17 | telemetry_entry 18 | |> cast(attrs, [:time, :monitor_id, :account_id, :instance_id, :check_id, :value]) 19 | |> validate_required([:time, :monitor_id, :account_id, :instance_id, :check_id, :value]) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/backend/telemetry_repo.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.TelemetryRepo do 2 | use Ecto.Repo, 3 | otp_app: :backend, 4 | adapter: Ecto.Adapters.Postgres 5 | end 6 | 7 | 8 | defmodule Backend.TelemetrySourceRepo do 9 | @moduledoc """ 10 | Separate stub connection for Telemetry.ToLocal server so that we can connect to source to copy telem 11 | """ 12 | use Ecto.Repo, 13 | otp_app: :backend, 14 | adapter: Ecto.Adapters.Postgres 15 | end 16 | 17 | 18 | defmodule Backend.TelemetryWriteRepo do 19 | @moduledoc """ 20 | The write connection for telemetry 21 | """ 22 | 23 | use Ecto.Repo, 24 | otp_app: :backend, 25 | adapter: Ecto.Adapters.Postgres 26 | 27 | require Logger 28 | 29 | def create_table_if_required() do 30 | File.read!("./sql/timescale-create-hypertable.sql") 31 | |> String.split(";") 32 | |> Enum.each(&Ecto.Adapters.SQL.query!(Backend.TelemetryWriteRepo, &1)) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/backend/test_data.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.TestData do 2 | @moduledoc """ 3 | Test data for the monitoring report thing. The test 4 | data was copy/pasted from a production GraphQL response. 5 | """ 6 | 7 | @awslambda "priv/testdata/awslambda.json" 8 | |> File.read!() 9 | |> Jason.decode!(keys: :atoms) 10 | 11 | @pagerduty "priv/testdata/pagerduty.json" 12 | |> File.read!() 13 | |> Jason.decode!(keys: :atoms) 14 | 15 | def get_report("awslambda") do 16 | @awslambda.data 17 | end 18 | def get_report("pagerduty") do 19 | @pagerduty.data 20 | end 21 | def get_report(mon) do 22 | raise "get report is not implemented for #{mon}!" 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/backend/twitter.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.Twitter do 2 | @moduledoc """ 3 | API for working with the Twitter counter subsystem 4 | """ 5 | 6 | @doc """ 7 | Return the counts as an array of `{timestamp, value}` tuples for 8 | the indicated monitor/hashtag. 9 | """ 10 | require Logger 11 | 12 | def counts(monitor_logical_name, hashtag) do 13 | worker_name = Backend.Twitter.Supervisor.worker_name(monitor_logical_name, hashtag) 14 | if Swarm.whereis_name(worker_name) == :undefined do 15 | Logger.warn("Backend.Twitter.counts/2 cannot find worker for #{inspect {monitor_logical_name, hashtag}}") 16 | [] 17 | else 18 | Backend.Twitter.Supervisor.worker_name(monitor_logical_name, hashtag, :via_tuple) 19 | |> Backend.Twitter.Worker.counts() 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/backend/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule Backend.Utils do 2 | 3 | @type retry_options::[max_attempts: pos_integer(), sleep_time: pos_integer(), success_check: (any() -> boolean())] 4 | 5 | @spec do_with_retries((pos_integer() -> any()), retry_options) :: {:error, :max_retries} | {:ok, any} 6 | def do_with_retries(action, opts \\ []) do 7 | max_attempts = Keyword.get(opts, :max_attempts, 10) 8 | sleep_time = Keyword.get(opts, :sleep_time, 1_000) 9 | 10 | success_check = Keyword.get(opts, :success_check, & !is_nil(&1)) 11 | 12 | do_with_retries(action, success_check, 1, max_attempts, sleep_time) 13 | end 14 | 15 | defp do_with_retries(_action, _success_check, current_attempts, max_attempts, _sleep_time) when current_attempts > max_attempts, do: {:error, :max_retries} 16 | defp do_with_retries(action, success_check, current_attempts, max_attempts, sleep_time) do 17 | result = action.(current_attempts) 18 | 19 | if success_check.(result) do 20 | {:ok, result} 21 | else 22 | if sleep_time > 0, do: Process.sleep(sleep_time) 23 | 24 | do_with_retries(action, success_check, current_attempts + 1, max_attempts, sleep_time) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/backend_web/api/api_spec.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.API.ApiSpec do 2 | alias OpenApiSpex.{Components, Info, OpenApi, Paths, SecurityScheme} 3 | alias BackendWeb.Router 4 | @behaviour OpenApi 5 | 6 | @impl OpenApi 7 | def spec do 8 | %OpenApi{ 9 | info: %Info{ 10 | title: "Metrist API", 11 | version: "0", 12 | description: "API Documentation" 13 | }, 14 | # Populate the paths from a phoenix router 15 | paths: Paths.from_router(Router), 16 | components: %Components{ 17 | securitySchemes: %{"authorization" => %SecurityScheme{type: "http", scheme: "bearer"}} 18 | }, 19 | security: [%{"authorization" => []}] 20 | } 21 | |> OpenApiSpex.resolve_schema_modules() # Discover request/response schemas from path specs 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/backend_web/api/pagination_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.API.PaginationHelpers do 2 | def metadata_json(%Paginator.Page.Metadata{} = metadata) do 3 | %{cursor_after: metadata.after, cursor_before: metadata.before} 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/backend_web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "room:*", BackendWeb.RoomChannel 6 | 7 | # Socket params are passed from the client and can 8 | # be used to verify and authenticate a user. After 9 | # verification, you can put default assigns into 10 | # the socket that will be set for all channels, ie 11 | # 12 | # {:ok, assign(socket, :user_id, verified_user_id)} 13 | # 14 | # To deny connection, return `:error`. 15 | # 16 | # See `Phoenix.Token` documentation for examples in 17 | # performing token verification on connect. 18 | @impl true 19 | def connect(_params, socket, _connect_info) do 20 | {:ok, socket} 21 | end 22 | 23 | # Socket id's are topics that allow you to identify all sockets for a given user: 24 | # 25 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}" 26 | # 27 | # Would allow you to broadcast a "disconnect" event and terminate 28 | # all active sockets and channels for a given user: 29 | # 30 | # BackendWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) 31 | # 32 | # Returning `nil` makes this socket anonymous. 33 | @impl true 34 | def id(_socket), do: nil 35 | end 36 | -------------------------------------------------------------------------------- /lib/backend_web/components/doc_popup.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.Components.DocPopup do 2 | @moduledoc """ 3 | Simple pop-up to display documentation strings. 4 | """ 5 | use BackendWeb, :component 6 | 7 | def render(assigns = %{module: module, tag: tag}) do 8 | description = 9 | tag 10 | |> module.description() 11 | |> String.replace(~r/\s+/, " ") 12 | 13 | assigns = assigns 14 | |> assign(:description, description) 15 | 16 | do_render(assigns) 17 | end 18 | 19 | # The simplest that could possible work for now: native browser tooltip. Having 20 | # an anchor here naturally extends into a "click for even more information" later on 21 | defp do_render(assigns = %{description: ""}), do: ~H"" 22 | defp do_render(assigns) do 23 | ~H""" 24 | 25 | ⓘ 26 | 27 | """ 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/backend_web/components/download.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.Components.Download do 2 | use BackendWeb, :live_component 3 | 4 | @impl true 5 | def mount(socket) do 6 | {:ok, socket} 7 | end 8 | 9 | @impl true 10 | def update(assigns, socket) do 11 | {:ok, 12 | socket 13 | |> assign(assigns) 14 | |> assign(url: "https://docs.metrist.io/guides/orchestrator-installation")} 15 | end 16 | 17 | # For now, we only render when logged in 18 | @impl true 19 | def render(assigns = %{current_user: nil}) do 20 | ~H""" 21 |
22 | """ 23 | end 24 | 25 | def render(assigns) do 26 | ~H""" 27 | 33 | """ 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/backend_web/components/safe_image.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.Components.SafeImage do 2 | use BackendWeb, :live_component 3 | 4 | # TODO: Handle errors loading image 5 | def mount(socket) do 6 | {:ok, assign(socket, 7 | src: "", 8 | class: "", 9 | alt: "")} 10 | end 11 | 12 | def render(assigns) do 13 | ~H""" 14 | {@alt} 15 | """ 16 | end 17 | 18 | 19 | defp img_src(nil), do: "/images/default.png" 20 | defp img_src(""), do: "/images/default.png" 21 | defp img_src(src), do: src 22 | end 23 | -------------------------------------------------------------------------------- /lib/backend_web/components/status_page/ui/page_component.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.Components.StatusPage.UI.PageComponent do 2 | @moduledoc """ 3 | Utility module for mapping various states to CSS properties needed by the status page component UI 4 | """ 5 | @state_up :up 6 | @state_degraded :degraded 7 | @state_issues :issues 8 | @state_down :down 9 | @state_maintenance :maintenance 10 | @state_blocked :blocked 11 | 12 | def class(%{type: :spinner, state: state}) 13 | when state in [ 14 | @state_up, 15 | @state_degraded, 16 | @state_issues, 17 | @state_down, 18 | @state_maintenance, 19 | @state_blocked 20 | ], 21 | do: "text-#{state_to_css(state)}" 22 | 23 | def class(_), do: "text-healthy" 24 | 25 | defp state_to_css(:up), do: "healthy" 26 | defp state_to_css(:down), do: "down" 27 | defp state_to_css(:issues), do: "issues" 28 | defp state_to_css(:degraded), do: "degraded" 29 | defp state_to_css(:blocked), do: "blocked" 30 | defp state_to_css(:maintenance), do: "blue-shade" 31 | end 32 | -------------------------------------------------------------------------------- /lib/backend_web/controllers/fallback_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.FallbackController do 2 | use Phoenix.Controller 3 | 4 | def call(conn, {:error, :not_found}) do 5 | conn 6 | |> put_status(:not_found) 7 | |> put_view(BackendWeb.ErrorView) 8 | |> render(:"404") 9 | end 10 | 11 | def call(conn, {:error, :forbidden}) do 12 | conn 13 | |> put_status(403) 14 | |> put_view(BackendWeb.ErrorView) 15 | |> render(:"403") 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/backend_web/controllers/health_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.HealthController do 2 | use BackendWeb, :controller 3 | 4 | def index(conn, _params) do 5 | text(conn, "OK\n") 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/backend_web/controllers/monitor_list_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.MonitorListController do 2 | use BackendWeb, :controller 3 | use OpenApiSpex.ControllerSpecs 4 | plug OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true, replace_params: false 5 | 6 | alias BackendWeb.API.Schemas.MonitorListResponse 7 | 8 | require Logger 9 | 10 | operation :get, 11 | summary: "Get a list of monitors for your account", 12 | tags: ["Monitors"], 13 | responses: [ 14 | ok: {"List of monitors", "application/json", MonitorListResponse} 15 | ] 16 | 17 | @doc """ 18 | Return errors of one or more monitors. 19 | """ 20 | def get(conn, _params) do 21 | account_id = get_session(conn, :account_id) 22 | 23 | monitor_list = 24 | account_id 25 | |> Backend.Projections.list_monitors() 26 | |> Enum.map(fn m -> %{name: m.name, logical_name: m.logical_name} end) 27 | 28 | Backend.Projections.register_api_hit(account_id) 29 | json(conn, monitor_list) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/backend_web/controllers/redirect_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.RedirectController do 2 | @moduledoc """ 3 | Redirects route based on the given `to` path 4 | 5 | ## Example 6 | get "/monitors", RedirectController, :index, assigns: %{to: "/"} 7 | """ 8 | 9 | use BackendWeb, :controller 10 | 11 | def index(conn, _params) do 12 | redirect(conn, to: conn.assigns.to) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/backend_web/controllers/signup_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.SignupController do 2 | use BackendWeb, :controller 3 | 4 | def index(conn, _params) do 5 | conn 6 | |> put_session(:auth_redirect, Routes.signup_path(conn, :monitors)) 7 | |> redirect(to: Routes.login_path(BackendWeb.Endpoint, :signup)) 8 | end 9 | 10 | def signup_redirect(conn, %{"connection" => connection}) do 11 | conn 12 | |> put_session(:auth_redirect, Routes.signup_path(conn, :monitors)) 13 | |> redirect(to: Routes.auth_path(BackendWeb.Endpoint, :request, "auth0", screen_hint: "login", connection: connection)) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/backend_web/controllers/snapshot_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.SnapshotController do 2 | use BackendWeb, :controller 3 | 4 | action_fallback BackendWeb.FallbackController 5 | 6 | require Logger 7 | 8 | def get(conn, %{"AccountId" => account_id, "Name" => monitor_logical_name}) do 9 | with {:ok, snapshot} <- Backend.RealTimeAnalytics.get_snapshot(account_id, monitor_logical_name) do 10 | json(conn, snapshot) 11 | end 12 | end 13 | 14 | def list(conn, %{"account" => account_id}) do 15 | snapshots = Backend.Projections.list_monitors(account_id) 16 | |> Enum.map(& Backend.RealTimeAnalytics.get_snapshot_or_nil(account_id, &1.logical_name)) 17 | |> Enum.reject(&is_nil/1) 18 | 19 | json(conn, snapshots) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/backend_web/controllers/subscription_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.SubscriptionController do 2 | use BackendWeb, :controller 3 | 4 | def delete(conn, %{"token" => token}) do 5 | conn = case BackendWeb.Token.verify_and_validate(token) do 6 | {:ok, claims} -> do_unsubscribe(conn, claims) 7 | _ -> put_flash(conn, :error, "Invalid unsubscribe token") 8 | end 9 | 10 | render(conn, "unsubscribe.html", spoofing?: false) 11 | end 12 | 13 | def delete(conn, _) do 14 | conn 15 | |> put_flash(:error, "Missing unsubscribe token") 16 | |> render("unsubscribe.html", spoofing?: false) 17 | end 18 | 19 | defp do_unsubscribe(conn, %{"accountId" => account_id, "subscriptionId" => subscription_id, "action" => "unsubscribe"}) do 20 | cmd = %Domain.Account.Commands.DeleteSubscriptions{ 21 | id: account_id, 22 | subscription_ids: [subscription_id] 23 | } 24 | 25 | case Backend.App.dispatch(cmd) do 26 | :ok -> put_flash(conn, :info, "Subscription successfully removed!") 27 | {:error, _} -> put_flash(conn, :error, "Error removing subscription") 28 | end 29 | end 30 | defp do_unsubscribe(conn, _), do: put_flash(conn, :error, "Invalid unsubscribe token") 31 | end 32 | -------------------------------------------------------------------------------- /lib/backend_web/controllers/verify_auth_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.VerifyAuthController do 2 | use BackendWeb, :controller 3 | use OpenApiSpex.ControllerSpecs 4 | plug OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true, replace_params: false 5 | 6 | require Logger 7 | 8 | operation :get, 9 | summary: "Check if the API token key works, return 200 if it does", 10 | tags: ["Other"], 11 | responses: [ 12 | ok: {"Valid API key", "text/plain", %OpenApiSpex.Schema{ type: :string }} 13 | ] 14 | 15 | @doc """ 16 | Return errors of one or more monitors. 17 | """ 18 | def get(conn, _params) do 19 | json(conn, :ok) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/backend_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import BackendWeb.Gettext 9 | 10 | # Simple translation 11 | gettext("Here is the string to translate") 12 | 13 | # Plural translation 14 | ngettext("Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3) 17 | 18 | # Domain-based translation 19 | dgettext("errors", "Here is the error message to translate") 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :backend 24 | end 25 | -------------------------------------------------------------------------------- /lib/backend_web/i18n.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.I18n do 2 | 3 | # Cheapest way from A->B, borrow the old Vue translations file. 4 | @external_resource "priv/en.json" 5 | 6 | @en "priv/en.json" 7 | |> File.read!() 8 | |> Jason.decode!() 9 | 10 | def str(s) do 11 | get_in(@en, String.split(s, ".")) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/backend_web/live/admin/utilities/snapshot_view_live.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | Snapshot Viewer 5 |

6 |
7 | 8 |
9 | 10 | Account 11 | 14 | 15 | <%= if @selected_account do %> 16 | Monitor 17 | 20 | <% end %> 21 |
22 | 23 |
24 |
25 |       <%= @state %>
26 |     
27 |
28 | <%= if @analyzer_config != "" do %> 29 |
30 |
31 |
32 |       <%= @analyzer_config %>
33 |     
34 |
35 | <% end %> 36 | 37 |
38 | -------------------------------------------------------------------------------- /lib/backend_web/live/apps_live.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.AppsLive do 2 | use BackendWeb, :live_view 3 | 4 | def mount(_params, _session, socket) do 5 | socket = 6 | socket 7 | |> assign(page_title: "Applications") 8 | 9 | {:ok, socket} 10 | end 11 | 12 | def handle_event("start-slack", _params, socket) do 13 | {:noreply, push_navigate(socket, 14 | to: Routes.apps_slack_path(socket, :start))} 15 | end 16 | 17 | def handle_event("start-teams", _params, socket) do 18 | {:noreply, push_navigate(socket, 19 | to: Routes.apps_teams_path(socket, :connect))} 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/backend_web/live/apps_live.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | <%= str("pages.applications.title") %> 5 |

6 |
7 | 8 |
    9 |
  • 10 | 13 |
  • 14 |
15 |
16 | -------------------------------------------------------------------------------- /lib/backend_web/live/apps_slack_live.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= if not @current_user.is_read_only do %> 4 | <%= step_progress(str("pages.apps.slack.steps"), step_of(@live_action)) %> 5 | <% end %> 6 |

7 | <%= @step_title %> 8 |

9 |
10 | 11 | <%= @step_contents %> 12 |
-------------------------------------------------------------------------------- /lib/backend_web/live/apps_teams_live.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= if not @current_user.is_read_only do %> 4 | <%= step_progress(str("pages.apps.teams.steps"), step_of(@live_action)) %> 5 | <% end %> 6 |

7 | <%= @step_title %> 8 |

9 |
10 | 11 | <%= @step_contents %> 12 |
-------------------------------------------------------------------------------- /lib/backend_web/live/datadog/auth_complete_live.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.Datadog.AuthCompleteLive do 2 | use BackendWeb, :dd_live_view 3 | 4 | @impl true 5 | def mount(_params, _session, socket) do 6 | {:ok, socket} 7 | end 8 | 9 | @impl true 10 | def handle_params(params, _, socket) do 11 | {:noreply, 12 | socket 13 | |> assign(auto_close: params["auto-close"] == "true")} 14 | end 15 | 16 | @impl true 17 | def render(assigns) do 18 | ~H""" 19 |
20 |
21 |

You are authorized

22 |

23 | This window will close automatically 24 | You may now close this tab 25 |

26 |
27 |
28 | """ 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/backend_web/live/datadog/controller_live.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.Datadog.ControllerLive do 2 | @moduledoc """ 3 | Controller iFrame for Datadog Metrist App 4 | """ 5 | use BackendWeb, :dd_live_view 6 | require Logger 7 | 8 | @impl true 9 | def mount(_params, _session, socket) do 10 | {:ok, socket} 11 | end 12 | 13 | @impl true 14 | def render(assigns) do 15 | ~H""" 16 |
17 |
18 | """ 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/backend_web/live/datadog/start_live.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.Datadog.StartLive do 2 | use BackendWeb, :dd_live_view 3 | 4 | require Logger 5 | 6 | @impl true 7 | def mount(_params, _session, socket) do 8 | {:ok, socket} 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/backend_web/live/datadog/start_live.html.heex: -------------------------------------------------------------------------------- 1 |
2 |

Metrist

3 | 4 |

Welcome to the Metrist Datadog App.

5 | 6 | <.link navigate={Routes.synthetics_wizard_path(@socket, :start)} class="p-3"> 7 | 10 | 11 |
12 | -------------------------------------------------------------------------------- /lib/backend_web/live/modal_component.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.ModalComponent do 2 | use BackendWeb, :live_component 3 | 4 | @impl true 5 | def render(assigns) do 6 | ~H""" 7 | 19 | """ 20 | end 21 | 22 | @impl true 23 | def handle_event("close", _, socket) do 24 | {:noreply, push_patch(socket, to: socket.assigns.return_to)} 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/backend_web/live/playground_live.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.PlaygroundLive do 2 | use BackendWeb, :live_view 3 | 4 | def mount(_params, _session, socket) do 5 | socket = 6 | socket 7 | |> assign(page_title: "Component Playground") 8 | 9 | {:ok, socket} 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/backend_web/live/playground_live.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | Component Playground 5 |

6 |
7 | 8 |
9 | 10 | <.dropdown placement="right"> 11 | <.dropdown_menu_item> 12 | 13 | Button item with icon 14 | 15 | <.dropdown_menu_item label="Item 2" class="!dark:text-gray-50"/> 16 | 17 | 18 | 19 | 20 |
21 | <.spinner size="sm" class="mr-1"/>Hello 22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /lib/backend_web/live/signup_live.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 | We're sorry. Metrist is not accepting new accounts at this time. 4 |
5 |
6 | -------------------------------------------------------------------------------- /lib/backend_web/templates/layout/app.html.heex: -------------------------------------------------------------------------------- 1 | <% # This is only used for signup, so "fake" the navbar etc. %> 2 |
3 | 10 |
11 | 12 | <.container class="my-10 font-lato"> 13 | <.alert 14 | color="info" 15 | class="mb-5" 16 | label={get_flash(@conn, :info)} 17 | phx-click="lv:clear-flash" 18 | phx-value-key="info" 19 | /> 20 | 21 | <.alert 22 | color="danger" 23 | class="mb-5" 24 | label={get_flash(@conn, :error)} 25 | phx-click="lv:clear-flash" 26 | phx-value-key="error" 27 | /> 28 | 29 | <%= @inner_content %> 30 | 31 | 32 |
33 | -------------------------------------------------------------------------------- /lib/backend_web/templates/layout/live_blank.html.heex: -------------------------------------------------------------------------------- 1 | <%= @inner_content %> 2 | -------------------------------------------------------------------------------- /lib/backend_web/templates/layout/live_dd.html.heex: -------------------------------------------------------------------------------- 1 |
2 | <%= @inner_content %> 3 |
4 | -------------------------------------------------------------------------------- /lib/backend_web/templates/page/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 |

<%= gettext "Welcome to %{name}!", name: "Phoenix" %>

3 |

Peace of mind from prototype to production

4 |
5 | 6 |
7 | 21 | 38 |
39 | -------------------------------------------------------------------------------- /lib/backend_web/templates/subscription/unsubscribe.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /lib/backend_web/token.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.Token do 2 | use Joken.Config 3 | end 4 | -------------------------------------------------------------------------------- /lib/backend_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.ErrorView do 2 | use BackendWeb, :view 3 | 4 | # If you want to customize a particular status code 5 | # for a certain format, you may uncomment below. 6 | # def render("500.html", _assigns) do 7 | # "Internal Server Error" 8 | # end 9 | 10 | # By default, Phoenix returns the status message from 11 | # the template name. For example, "404.html" becomes 12 | # "Not Found". 13 | def template_not_found(template, _assigns) do 14 | Phoenix.Controller.status_message_from_template(template) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/backend_web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.LayoutView do 2 | use BackendWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/backend_web/views/login_view.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.LoginView do 2 | use BackendWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/backend_web/views/page_view.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.PageView do 2 | use BackendWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/backend_web/views/signup_view.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.SignupView do 2 | use BackendWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/backend_web/views/subscription_view.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.SubscriptionView do 2 | use BackendWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/domain/crypt_repo.ex: -------------------------------------------------------------------------------- 1 | defmodule Domain.CryptRepo do 2 | @moduledoc """ 3 | Abstraction and helpers for repositories for key information. The actual implementations 4 | live in Backend. 5 | """ 6 | 7 | @doc """ 8 | The currently configured repository. Only one can be active application-wide. 9 | """ 10 | def current do 11 | Application.get_env(:backend, :crypt_repo, Backend.Crypto.EctoRepo) 12 | end 13 | 14 | @typedoc """ 15 | Key type, a tuple of "{id, scheme, key}" data. 16 | """ 17 | @type key_data :: {String.t(), String.t(), String.t()} 18 | 19 | @doc """ 20 | Initialize the repository. Called early in application startup for the currently configured repository. 21 | """ 22 | @callback initialize() :: :ok 23 | 24 | @doc """ 25 | Fetch or generate the key for the specified owner. Used for encryption operations; if multiple keys 26 | are active, a random one can be returned. If the repository has no key, one is generated on the fly for 27 | the current scheme. 28 | """ 29 | @callback key_for(owner_type :: String.t() | atom, id :: String.t()) :: key_data() 30 | 31 | @doc """ 32 | Retrieve the key by its id. 33 | """ 34 | @callback get(id :: String.t()) :: key_data() 35 | end 36 | -------------------------------------------------------------------------------- /lib/domain/datadog_grants.ex: -------------------------------------------------------------------------------- 1 | defmodule Domain.DatadogGrants do 2 | @derive Jason.Encoder 3 | defstruct [ 4 | :id, 5 | :user_id 6 | ] 7 | 8 | alias __MODULE__.Commands 9 | alias __MODULE__.Events 10 | 11 | def execute(%__MODULE__{}, c = %Commands.RequestGrant{}) do 12 | %Events.GrantRequested{id: c.id, user_id: c.user_id, verifier: c.verifier} 13 | end 14 | 15 | def execute( 16 | self = %__MODULE__{}, 17 | c = %Commands.UpdateGrant{} 18 | ) do 19 | %Events.GrantUpdated{ 20 | id: self.id, 21 | access_token: c.access_token, 22 | refresh_token: c.refresh_token, 23 | scope: c.scope, 24 | expires_in: c.expires_in 25 | } 26 | end 27 | 28 | def apply(self = %__MODULE__{}, e = %Events.GrantRequested{}) do 29 | %__MODULE__{ 30 | self 31 | | id: e.id, 32 | user_id: e.user_id 33 | } 34 | end 35 | 36 | def apply( 37 | self = %__MODULE__{}, 38 | %Events.GrantUpdated{} 39 | ) do 40 | self 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/domain/datadog_grants/commands.ex: -------------------------------------------------------------------------------- 1 | defmodule Domain.DatadogGrants.Commands do 2 | use TypedStruct 3 | 4 | typedstruct module: RequestGrant, enforce: true do 5 | use Domo 6 | field :id, String.t() 7 | field :user_id, String.t() 8 | field :verifier, String.t() 9 | end 10 | 11 | typedstruct module: UpdateGrant, enforce: true do 12 | use Domo 13 | field :id, String.t() 14 | field :verifier, String.t() 15 | field :access_token, String.t() 16 | field :refresh_token, String.t() 17 | field :scope, [String.t()] 18 | field :expires_in, integer() 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/domain/datadog_grants/events.ex: -------------------------------------------------------------------------------- 1 | defmodule Domain.DatadogGrants.Events do 2 | use TypedStruct 3 | 4 | typedstruct module: GrantRequested, enforce: true do 5 | @derive Jason.Encoder 6 | field :id, String.t() 7 | field :user_id, String.t() 8 | field :verifier, String.t() 9 | end 10 | 11 | typedstruct module: GrantUpdated, enforce: true do 12 | @derive Jason.Encoder 13 | field :id, String.t() 14 | field :access_token, String.t() 15 | field :refresh_token, String.t() 16 | field :scope, [String.t()] 17 | field :expires_in, integer() 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/domain/flow/commands.ex: -------------------------------------------------------------------------------- 1 | defmodule Domain.Flow.Commands do 2 | use TypedStruct 3 | 4 | typedstruct module: Create, enforce: true do 5 | use Domo 6 | field :id, String.t() 7 | field :name, String.t() 8 | field :timeout_minute, pos_integer() 9 | field :steps, [String.t()] 10 | end 11 | 12 | typedstruct module: Step, enforce: true do 13 | use Domo 14 | field :id, String.t() 15 | field :step, String.t() 16 | end 17 | 18 | typedstruct module: Timeout, enforce: true do 19 | use Domo 20 | field :id, String.t() 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/domain/flow/events.ex: -------------------------------------------------------------------------------- 1 | defmodule Domain.Flow.Events do 2 | use TypedStruct 3 | 4 | typedstruct module: Created, enforce: true do 5 | plugin Backend.JsonUtils 6 | field :id, String.t() 7 | field :name, String.t() 8 | field :timeout_minute, pos_integer() 9 | field :steps, [String.t()] 10 | end 11 | 12 | typedstruct module: StepCompleted, enforce: true do 13 | plugin Backend.JsonUtils 14 | field :id, String.t() 15 | field :step, String.t() 16 | field :completed, pos_integer() 17 | end 18 | 19 | typedstruct module: FlowCompleted, enforce: true do 20 | plugin Backend.JsonUtils 21 | field :id, String.t() 22 | end 23 | 24 | typedstruct module: FlowTimedOut, enforce: true do 25 | plugin Backend.JsonUtils 26 | field :id, String.t() 27 | field :at_step, pos_integer() 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/domain/id.ex: -------------------------------------------------------------------------------- 1 | defmodule Domain.Id do 2 | @current_version "1" 3 | 4 | @moduledoc """ 5 | Compact, database friendly ids 6 | """ 7 | 8 | def new() do 9 | @current_version 10 | <> ( 11 | :erlang.system_time(:nanosecond) 12 | |> Base62.encode()) 13 | <> ( 14 | :crypto.strong_rand_bytes(8) 15 | |> :binary.decode_unsigned() |> Base62.encode()) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/domain/issue/commands.ex: -------------------------------------------------------------------------------- 1 | defmodule Domain.Issue.Commands do 2 | use TypedStruct 3 | 4 | typedstruct module: EmitIssue do 5 | use Domo 6 | 7 | field :id, String.t(), enforce: true 8 | field :account_id, String.t() 9 | field :state, Backend.RealTimeAnalytics.Snapshotting.state() 10 | field :source, Domain.IssueTracker.issue_source() 11 | field :source_id, String.t() 12 | field :service, String.t() 13 | 14 | # Monitor Events 15 | field :region, String.t() 16 | field :check_logical_name, String.t() 17 | 18 | # Status Page Events 19 | field :component_id, String.t() 20 | 21 | field :start_time, NaiveDateTime.t() 22 | field :end_time, NaiveDateTime.t() 23 | end 24 | 25 | typedstruct module: RemoveIssueSource do 26 | field :id, String.t(), enforce: true 27 | field :source, Domain.IssueTracker.issue_source() 28 | 29 | # Monitor Events 30 | field :check_logical_name, String.t() 31 | 32 | # Status Page Events 33 | field :component_id, String.t() 34 | 35 | field :time, NaiveDateTime.t() 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/domain/middleware/type_validation.ex: -------------------------------------------------------------------------------- 1 | defmodule Domain.Middleware.TypeValidation do 2 | @behaviour Commanded.Middleware 3 | 4 | alias Commanded.Middleware.Pipeline 5 | 6 | def before_dispatch(%Pipeline{command: command = %module{}} = pipeline) do 7 | if Keyword.has_key?(module.__info__(:functions), :ensure_type!) do 8 | module.ensure_type!(command) 9 | end 10 | pipeline 11 | end 12 | 13 | def after_dispatch(pipeline), do: pipeline 14 | def after_failure(pipeline), do: pipeline 15 | end 16 | -------------------------------------------------------------------------------- /lib/domain/notice/commands.ex: -------------------------------------------------------------------------------- 1 | defmodule Domain.Notice.Commands do 2 | use TypedStruct 3 | 4 | typedstruct module: Create do 5 | use Domo 6 | field :id, String.t(), enforce: true 7 | field :monitor_id, String.t() 8 | field :summary, String.t(), enforce: true 9 | field :description, String.t() 10 | field :end_date, NaiveDateTime.t() 11 | end 12 | 13 | typedstruct module: Update do 14 | use Domo 15 | field :id, String.t(), enforce: true 16 | field :summary, String.t(), enforce: true 17 | field :description, String.t() 18 | field :end_date, NaiveDateTime.t() 19 | end 20 | 21 | typedstruct module: Clear do 22 | use Domo 23 | field :id, String.t(), enforce: true 24 | end 25 | 26 | typedstruct module: MarkRead, enforce: true do 27 | use Domo 28 | field :id, String.t() 29 | field :user_id, String.t() 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/domain/notice/events.ex: -------------------------------------------------------------------------------- 1 | defmodule Domain.Notice.Events do 2 | use TypedStruct 3 | 4 | typedstruct module: Created do 5 | plugin Backend.JsonUtils 6 | field :id, String.t(), enforce: true 7 | field :monitor_id, String.t() 8 | field :summary, String.t(), enforce: true 9 | field :description, String.t() 10 | field :end_date, DateTime.t() 11 | end 12 | 13 | typedstruct module: ContentUpdated do 14 | plugin Backend.JsonUtils 15 | field :id, String.t(), enforce: true 16 | field :summary, String.t(), enforce: true 17 | field :description, String.t() 18 | end 19 | 20 | typedstruct module: EndDateUpdated, enforce: true do 21 | plugin Backend.JsonUtils 22 | field :id, String.t() 23 | field :end_date, DateTime.t() 24 | end 25 | 26 | typedstruct module: MarkedRead, enforce: true do 27 | plugin Backend.JsonUtils 28 | field :id, String.t() 29 | field :user_id, String.t() 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/domain/slack_integration/commands.ex: -------------------------------------------------------------------------------- 1 | defmodule Domain.SlackIntegration.Commands do 2 | use TypedStruct 3 | 4 | typedstruct module: RequestConnection, enforce: true do 5 | use Domo 6 | field :id, String.t 7 | field :account_id, String.t 8 | field :redirect_to, String.t, enforce: false 9 | end 10 | 11 | typedstruct module: CompleteConnection, enforce: true do 12 | use Domo 13 | field :id, String.t 14 | field :code, String.t 15 | end 16 | 17 | typedstruct module: FailConnection, enforce: true do 18 | use Domo 19 | field :id, String.t 20 | field :reason, String.t 21 | field :existing_account_id, String.t, enforce: false 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/domain/slack_integration/events.ex: -------------------------------------------------------------------------------- 1 | defmodule Domain.SlackIntegration.Events do 2 | use TypedStruct 3 | 4 | typedstruct module: ConnectionRequested, enforce: true do 5 | @derive Jason.Encoder 6 | field :id, String.t 7 | field :account_id, String.t 8 | field :redirect_to, String.t, enforce: false 9 | end 10 | 11 | typedstruct module: ConnectionCompleted, enforce: true do 12 | @derive Jason.Encoder 13 | field :id, String.t 14 | field :account_id, String.t 15 | field :code, String.t 16 | field :redirect_to, String.t, enforce: false 17 | end 18 | 19 | typedstruct module: ConnectionFailed, enforce: true do 20 | @derive Jason.Encoder 21 | field :id, String.t 22 | field :reason, String.t 23 | field :existing_account_id, String.t, enforce: false 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/domain/tagging.ex: -------------------------------------------------------------------------------- 1 | defmodule Domain.Tagging do 2 | @moduledoc """ 3 | Helpers for tagging. See ADR-0014 for details. 4 | """ 5 | 6 | # Note that this will allow some malformed keys to pass through. For now, 7 | # we don't care too much. 8 | @kv_re ~r/^(?[a-zA-Z0-9:.]+):(?[^:]+)$/ 9 | 10 | @doc """ 11 | If true, this is a legacy tag. Legacy tags are the tags 12 | we used initially for grouping monitors on the dashboard: 13 | aws, gcp, azure, ... 14 | """ 15 | def is_legacy?(tag) 16 | when tag in ["aws", "azure", "gcp", "api", "infrastructure", "saas", "other"], 17 | do: true 18 | 19 | def is_legacy?(_tag), do: false 20 | 21 | @doc """ 22 | If true, this is a valid tag. We don't consider legacy tags to be valid. 23 | """ 24 | def is_valid?(tag), do: Regex.match?(@kv_re, tag) 25 | 26 | @doc """ 27 | If true, this is a standard tag 28 | """ 29 | def is_standard?(tag), do: not is_legacy?(tag) and is_valid?(tag) 30 | 31 | @doc """ 32 | Return a tuple with the key and value 33 | """ 34 | def kv(tag) do 35 | split = Regex.named_captures(@kv_re, tag) 36 | {Map.get(split, "key"), Map.get(split, "value")} 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/mix/tasks/metrist/dbpa.migrate.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Metrist.Dbpa.Migrate do 2 | use Mix.Task 3 | 4 | @shortdoc "Runs all account migrations" 5 | 6 | @moduledoc """ 7 | This Mix task runs migrations in all account schemas. Only runs in the local environment for now. 8 | """ 9 | 10 | @impl true 11 | def run(_args) do 12 | Mix.Tasks.Metrist.Helpers.start_repos("local") 13 | 14 | Backend.Repo.migrate_all_tenants() 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/mix/tasks/metrist/dbpa.rollback.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Metrist.Dbpa.Rollback do 2 | use Mix.Task 3 | 4 | @shortdoc "Rolls back account migrations 1 step" 5 | 6 | @moduledoc """ 7 | This Mix task performs a 1 step rollback in all account schemas. Only runs in the local 8 | environment for now. 9 | """ 10 | 11 | @impl true 12 | def run(_args) do 13 | Mix.Tasks.Metrist.Helpers.start_repos("local") 14 | 15 | Backend.Repo.rollback_all_tenants() 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/mix/tasks/metrist/end_event.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Metrist.EndEvent do 2 | use Mix.Task 3 | 4 | 5 | alias Mix.Tasks.Metrist.Helpers 6 | @opts [ 7 | :dry_run, 8 | :env, 9 | :account_id, 10 | :monitor_logical_name, 11 | {:monitor_event_id, nil, :string, :mandatory, "The event ID that we want to end"}, 12 | {:end_time, nil, :integer, :mandatory, "When to end the event as an integer in Unix time (seconds)"} 13 | ] 14 | @shortdoc "Sets the name for a monitor" 15 | @moduledoc """ 16 | Sets a monitor's name field 17 | 18 | #{Helpers.gen_command_line_docs(@opts)} 19 | 20 | ## Examples: 21 | 22 | mix metrist.set_monitor_name -e dev1 -m testsignal -a SHARED --name "Test Signal" 23 | 24 | """ 25 | def run(args) do 26 | options = Helpers.parse_args(@opts, args) 27 | 28 | dt = %{DateTime.to_naive(DateTime.from_unix!(options.end_time)) | microsecond: {0, 6}} 29 | 30 | Mix.Tasks.Metrist.Helpers.send_command( 31 | %Domain.Monitor.Commands.EndEvent{ 32 | id: options.monitor_id, 33 | monitor_event_id: options.monitor_event_id, 34 | end_time: dt 35 | }, 36 | options.env, options.dry_run) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/mix/tasks/metrist/one_off/README.md: -------------------------------------------------------------------------------- 1 | # One-off Mix tasks 2 | 3 | This directory is for one-off Mix tasks. These typically are the result of a ticket needing roll-out 4 | steps that are easiest accomplish by manually invoking the tasks. 5 | 6 | Adding these mix tasks is fine. 7 | 8 | Maintaining them: not so much. 9 | 10 | Therefore, assume that the tasks here are broken: they use obsolete methods to interact with our backend, they 11 | invoke methods that don't exist, and so on. We keep them around just in case we need them for inspiration, 12 | but that's all. 13 | -------------------------------------------------------------------------------- /lib/mix/tasks/metrist/one_off/backfill_issue_sources.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Metrist.OneOff.BackfillIssueSources do 2 | use Mix.Task 3 | import Ecto.Query 4 | alias Mix.Tasks.Metrist.Helpers 5 | 6 | @shortdoc "Backfills sources for Issues from the associated IssueEvents" 7 | 8 | @opts [ 9 | :env, 10 | ] 11 | 12 | 13 | @moduledoc """ 14 | #{Helpers.gen_command_line_docs(@opts)} 15 | 16 | #{Helpers.mix_env_notice()} 17 | """ 18 | 19 | def run(args) do 20 | opts = Helpers.parse_args(@opts, args) 21 | Helpers.start_repos(opts.env) 22 | 23 | account_ids = Backend.Projections.Account.list_account_ids() 24 | 25 | for account_id <- account_ids do 26 | issues = from(i in Backend.Projections.Dbpa.Issue) 27 | |> put_query_prefix(Backend.Repo.schema_name(account_id)) 28 | |> preload([:events]) 29 | |> Backend.Repo.all() 30 | 31 | for issue <- issues do 32 | sources = issue.events 33 | |> Enum.map(& &1.source) 34 | |> Enum.uniq() 35 | 36 | issue 37 | |> Ecto.Changeset.change(sources: sources) 38 | |> Backend.Repo.update() 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/mix/tasks/metrist/one_off/reset_dev_status_page.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Metrist.OneOff.ResetDevStatusPage do 2 | use Mix.Task 3 | alias Mix.Tasks.Metrist.Helpers 4 | 5 | @shortdoc "MET-847 Reset the status page aggregate state in dev1" 6 | 7 | def run(_args) do 8 | env = "dev1" 9 | Helpers.start_repos(env) 10 | Backend.Projections.Dbpa.StatusPage.status_pages() 11 | |> Enum.map(fn status_page -> 12 | %Domain.StatusPage.Commands.Reset{id: status_page.id} 13 | end) 14 | |> Helpers.send_commands(env, false) 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /lib/mix/tasks/metrist/one_off/update_aws_temp_back_to_aws.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Metrist.OneOff.UpdateAwsTempBackToAws do 2 | use Mix.Task 3 | alias Mix.Tasks.Metrist.Helpers 4 | 5 | @shortdoc "MET-83 Moves all awstemp monitors back to aws" 6 | 7 | @opts [ 8 | :dry_run, 9 | :env 10 | ] 11 | 12 | def run(args) do 13 | options = Helpers.parse_args(@opts, args) 14 | Mix.Tasks.Metrist.Helpers.start_repos(options.env) 15 | 16 | "SHARED" 17 | |> Backend.Projections.get_monitor_configs() 18 | |> Enum.filter(fn monitor_config -> ("awstemp" in monitor_config.run_groups) end) 19 | |> Enum.map(fn monitor_config -> 20 | case Enum.find_index(monitor_config.run_groups, fn rg -> rg == "awstemp" end) do 21 | nil -> nil 22 | index -> %Domain.Monitor.Commands.SetRunGroups{ 23 | id: "SHARED_" <> monitor_config.monitor_logical_name, 24 | config_id: monitor_config.id, 25 | run_groups: List.replace_at(monitor_config.run_groups, index, "aws") 26 | } 27 | end 28 | end) 29 | |> Enum.reject(&(is_nil((&1)))) 30 | |> Helpers.send_commands(options.env, options.dry_run) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/mix/tasks/metrist/remove_monitor_config.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Metrist.RemoveMonitorConfig do 2 | use Mix.Task 3 | 4 | alias Mix.Tasks.Metrist.Helpers 5 | 6 | @shortdoc "Removes a monitor config by id." 7 | 8 | @opts [ 9 | :dry_run, 10 | :env, 11 | :account_id, 12 | :monitor_logical_name, 13 | :config_id 14 | ] 15 | 16 | @moduledoc """ 17 | #{@shortdoc} 18 | 19 | #{Helpers.gen_command_line_docs(@opts)} 20 | """ 21 | 22 | def run(args) do 23 | options = Helpers.parse_args(@opts, args) 24 | 25 | Helpers.send_command( 26 | %Domain.Monitor.Commands.RemoveConfig{ 27 | id: options.monitor_id, 28 | config_id: options.config_id 29 | }, 30 | options.env, options.dry_run 31 | ) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/mix/tasks/metrist/remove_slack_workspace.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Metrist.RemoveSlackWorkspace do 2 | use Mix.Task 3 | alias Mix.Tasks.Metrist.Helpers 4 | 5 | @opts [ 6 | :dry_run, 7 | :env, 8 | :account_id, 9 | {:workspace_id, nil, :string, :mandatory, "Slack workspace ID"} 10 | ] 11 | @shortdoc "Removes a Slack Workspace from an account" 12 | @moduledoc """ 13 | This is mostly for out of sync workspaces, i.e. any that were manually removed 14 | from slack without the change being applied to backend's data. 15 | 16 | #{Helpers.gen_command_line_docs(@opts)} 17 | 18 | ## Example 19 | 20 | mix metrist.remove_slack_workspace -e dev1 -a account_id --workspace-id workspace_id 21 | 22 | """ 23 | 24 | def run(args) do 25 | options = Helpers.parse_args(@opts, args) 26 | 27 | Mix.Tasks.Metrist.Helpers.send_command( 28 | %Domain.Account.Commands.RemoveSlackWorkspace{ 29 | id: options.account_id, 30 | team_id: options.workspace_id 31 | }, 32 | options.env, options.dry_run) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/mix/tasks/metrist/remove_status_page.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Metrist.RemoveStatusPage do 2 | use Mix.Task 3 | alias Mix.Tasks.Metrist.Helpers 4 | require Logger 5 | 6 | @opts [ 7 | :dry_run, 8 | :env, 9 | {:page, nil, :string, :mandatory, "Page to be removed"} 10 | ] 11 | @shortdoc "Removes a Status Page from the system" 12 | @moduledoc """ 13 | This will also remove projection data for the status page 14 | including subscriptions, component changes, components, 15 | and the status page itself 16 | 17 | #{Helpers.gen_command_line_docs(@opts)} 18 | 19 | #{Helpers.mix_env_notice()} 20 | 21 | ## Example 22 | 23 | MIX_ENV=prod mix metrist.remove_status_page -e dev1 --page fastly 24 | """ 25 | 26 | def run(args) do 27 | options = Helpers.parse_args(@opts, args) 28 | Mix.Tasks.Metrist.Helpers.start_repos(options.env) 29 | 30 | [ 31 | %Domain.StatusPage.Commands.Remove{ 32 | id: Backend.Projections.status_page_by_name(options.page).id 33 | } 34 | ] 35 | |> Helpers.send_commands(options.env, options.dry_run) 36 | 37 | end 38 | 39 | end 40 | -------------------------------------------------------------------------------- /lib/mix/tasks/metrist/remove_statuspage_component.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Metrist.RemoveStatuspageComponent do 2 | use Mix.Task 3 | alias Mix.Tasks.Metrist.Helpers 4 | 5 | @opts [ 6 | :dry_run, 7 | :env, 8 | {:id, nil, :string, :mandatory, "ID of the Status Page"}, 9 | {:name, nil, :string, :mandatory, "Name of the component to remove"}, 10 | 11 | ] 12 | 13 | def run(args) do 14 | options = Helpers.parse_args(@opts, args) 15 | Mix.Tasks.Metrist.Helpers.start_repos(options.env) 16 | 17 | cmd = %Domain.StatusPage.Commands.RemoveComponent{ 18 | id: options.id, 19 | component_name: options.name 20 | } 21 | 22 | Helpers.send_command(cmd, options.env, options.dry_run) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/mix/tasks/metrist/reset_status_page.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Metrist.ResetStatusPage do 2 | use Mix.Task 3 | alias Mix.Tasks.Metrist.Helpers 4 | require Logger 5 | 6 | @opts [ 7 | :dry_run, 8 | :env, 9 | {:page, nil, :string, :mandatory, "Page to be reset"} 10 | ] 11 | @shortdoc "Reset a status page aggregate" 12 | @moduledoc """ 13 | Resets a status page resetting aggregate state and removing the 14 | component projections. 15 | 16 | #{Helpers.gen_command_line_docs(@opts)} 17 | 18 | #{Helpers.mix_env_notice()} 19 | 20 | ## Example 21 | 22 | MIX_ENV=prod mix metrist.reset_status_page -e dev1 --page fastly 23 | """ 24 | 25 | def run(args) do 26 | options = Helpers.parse_args(@opts, args) 27 | Helpers.start_repos(options.env) 28 | status_page = Backend.Projections.status_page_by_name(options.page) 29 | 30 | [ 31 | %Domain.StatusPage.Commands.Reset{ 32 | id: status_page.id 33 | } 34 | ] 35 | |> Helpers.send_commands(options.env, options.dry_run) 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /lib/mix/tasks/metrist/set_check_name.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Metrist.SetCheckName do 2 | use Mix.Task 3 | 4 | 5 | alias Mix.Tasks.Metrist.Helpers 6 | @opts [ 7 | :dry_run, 8 | :env, 9 | :account_id, 10 | :monitor_logical_name, 11 | {:name, nil, :string, :mandatory, "Human readable name to give to the check"}, 12 | {:check_logical_name, nil, :string, :mandatory, "Check logical name to change"} 13 | ] 14 | @shortdoc "Sets the name for a check" 15 | @moduledoc """ 16 | Sets a check's name field 17 | 18 | #{Helpers.gen_command_line_docs(@opts)} 19 | 20 | ## Examples: 21 | 22 | mix metrist.set_check_name -e dev1 -m awsroute53 -a SHARED --check_logical_name "QueryExistingDNSRecord" --name "Query existing DNS record" 23 | 24 | """ 25 | def run(args) do 26 | options = Helpers.parse_args(@opts, args) 27 | 28 | Mix.Tasks.Metrist.Helpers.send_command( 29 | %Domain.Monitor.Commands.UpdateCheckName{ 30 | id: options.monitor_id, 31 | logical_name: options.check_logical_name, 32 | name: options.name 33 | }, 34 | options.env, options.dry_run) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/mix/tasks/metrist/set_interval_secs.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Metrist.SetIntervalSecs do 2 | use Mix.Task 3 | require Logger 4 | alias Mix.Tasks.Metrist.Helpers 5 | 6 | @shortdoc "Sets the interval for a monitor config" 7 | 8 | @opts [ 9 | :dry_run, 10 | :env, 11 | :account_id, 12 | :monitor_logical_name, 13 | :config_id, 14 | {:interval_secs, nil, :integer, :mandatory, "Seconds to wait between monitor runs"} 15 | ] 16 | 17 | @moduledoc """ 18 | Sets value for a monitor_config's `interval_secs` field 19 | 20 | #{Helpers.gen_command_line_docs(@opts)} 21 | 22 | ## Examples: 23 | 24 | mix metrist.set_interval_secs -e dev1 -m testsignal -c 11vpT2grcoxX5qg9ckLCqlP -a SHARED --interval-secs 600 25 | """ 26 | 27 | def run(args) do 28 | options = Helpers.parse_args(@opts, args) 29 | 30 | Helpers.send_command( 31 | %Domain.Monitor.Commands.SetIntervalSecs{ 32 | id: options.monitor_id, 33 | config_id: options.config_id, 34 | interval_secs: options.interval_secs 35 | }, 36 | options.env, options.dry_run 37 | ) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/mix/tasks/metrist/set_monitor_name.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Metrist.SetMonitorName do 2 | use Mix.Task 3 | 4 | 5 | alias Mix.Tasks.Metrist.Helpers 6 | @opts [ 7 | :dry_run, 8 | :env, 9 | :account_id, 10 | :monitor_logical_name, 11 | {:name, nil, :string, :mandatory, "Human readable name to give to the monitor"} 12 | ] 13 | @shortdoc "Sets the name for a monitor" 14 | @moduledoc """ 15 | Sets a monitor's name field 16 | 17 | #{Helpers.gen_command_line_docs(@opts)} 18 | 19 | ## Examples: 20 | 21 | mix metrist.set_monitor_name -e dev1 -m testsignal -a SHARED --name "Test Signal" 22 | 23 | """ 24 | def run(args) do 25 | options = Helpers.parse_args(@opts, args) 26 | 27 | Mix.Tasks.Metrist.Helpers.send_command( 28 | %Domain.Monitor.Commands.ChangeName{ 29 | id: options.monitor_id, 30 | name: options.name 31 | }, 32 | options.env, options.dry_run) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/mix/tasks/metrist/set_monitor_tag.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Metrist.SetMonitorTag do 2 | use Mix.Task 3 | alias Mix.Tasks.Metrist.Helpers 4 | 5 | @opts [ 6 | :dry_run, 7 | :env, 8 | :account_id, 9 | :monitor_logical_name, 10 | {:tag, nil, :string, :mandatory, "The tag to set"} 11 | ] 12 | @shortdoc "Sets the tag for a monitor" 13 | @moduledoc """ 14 | Sets a monitor's tag 15 | 16 | #{Helpers.gen_command_line_docs(@opts)} 17 | 18 | ## Examples: 19 | 20 | mix metrist.set_monitor_tag -e dev1 -m asana -a SHARED --tag "saas" 21 | 22 | """ 23 | def run(args) do 24 | options = Helpers.parse_args(@opts, args) 25 | 26 | Mix.Tasks.Metrist.Helpers.send_command( 27 | %Domain.Monitor.Commands.AddTag{ 28 | id: options.monitor_id, 29 | tag: options.tag 30 | }, 31 | options.env, options.dry_run) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/mix/tasks/metrist/set_run_group.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Metrist.SetRunGroup do 2 | use Mix.Task 3 | alias Mix.Tasks.Metrist.Helpers 4 | 5 | @opts [ 6 | :dry_run, 7 | :env, 8 | :account_id, 9 | :monitor_logical_name, 10 | :config_id, 11 | {:run_group, :r, :keep, :mandatory, "One or more run group names"} 12 | ] 13 | @shortdoc "Sets the rungroup for a monitor config" 14 | @moduledoc """ 15 | Sets values for a monitor_config's `run_groups` field 16 | 17 | #{Helpers.gen_command_line_docs(@opts)} 18 | 19 | ## Examples: 20 | 21 | mix metrist.set_run_group -e dev1 -m testsignal -c 11vpT2grcoxX5qg9ckLCqlP -a SHARED --run-group "RunDLL" --run-group "SecondRunGroup" 22 | 23 | mix metrist.set_run_group -e dev1 -m testsignal -c 11vpT2grcoxX5qg9ckLCqlP -a SHARED --run-group "RunDLL" 24 | """ 25 | def run(args) do 26 | options = Helpers.parse_args(@opts, args) 27 | 28 | Mix.Tasks.Metrist.Helpers.send_command( 29 | %Domain.Monitor.Commands.SetRunGroups{ 30 | id: options.monitor_id, 31 | config_id: options.config_id, 32 | run_groups: options.run_group}, 33 | options.env, options.dry_run) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/mix/tasks/metrist/set_twitter_hashtags.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Metrist.SetTwitterHashtags do 2 | use Mix.Task 3 | require Logger 4 | alias Mix.Tasks.Metrist.Helpers 5 | 6 | @opts [ 7 | :env, 8 | :account_id, 9 | :monitor_logical_name, 10 | {:hashtag, nil, :keep, [], 11 | "A hashtag for the monitor's Twitter timeline"} 12 | ] 13 | @shortdoc "Set twitter hashtags for a monitor" 14 | @moduledoc """ 15 | #{@shortdoc}. 16 | 17 | Note that this command will just set the list of hashtags to the ones that are specified on the 18 | command line here, it will not preserve current hashtags. Also note that for now, we will only 19 | use SHARED - specifying account_id is there for compatibility but currently not used. 20 | 21 | #{Helpers.gen_command_line_docs(@opts)} 22 | 23 | #{Helpers.mix_env_notice()} 24 | """ 25 | 26 | def run(args) do 27 | options = Helpers.parse_args(@opts, args) 28 | Helpers.start_repos(options.env) 29 | 30 | Helpers.send_command( 31 | %Domain.Monitor.Commands.SetTwitterHashtags{ 32 | id: options.monitor_id, 33 | hashtags: options.hashtag 34 | }, 35 | options.env) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/mix/tasks/metrist/update_user_account.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Metrist.UpdateUserAccount do 2 | use Mix.Task 3 | alias Mix.Tasks.Metrist.Helpers 4 | 5 | @opts [ 6 | :dry_run, 7 | :env, 8 | :account_id, 9 | {:user_id, nil, :string, :mandatory, "User id to update"} 10 | ] 11 | @shortdoc "Update a user's account" 12 | @moduledoc """ 13 | #{@shortdoc} 14 | 15 | #{Helpers.gen_command_line_docs(@opts)} 16 | """ 17 | 18 | def run(args) do 19 | options = Helpers.parse_args(@opts, args) 20 | 21 | Helpers.send_command( 22 | %Domain.User.Commands.Update{ 23 | id: options.user_id, 24 | user_account_id: options.account_id 25 | }, 26 | options.env, options.dry_run 27 | ) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/signals.ex: -------------------------------------------------------------------------------- 1 | defmodule Signals do 2 | 3 | def start() do 4 | :gen_event.swap_handler(:erl_signal_server, {:erl_signal_handler, []}, {__MODULE__, []}) 5 | end 6 | 7 | def init(_args) do 8 | IO.puts("Signals: Initializing signal handling") 9 | {:ok, []} 10 | end 11 | 12 | def handle_event(exit, state) when exit in [:sigterm, :sigusr1] do 13 | IO.puts("Signals: Asked to terminate by #{exit}") 14 | :init.stop() 15 | {:ok, state} 16 | end 17 | 18 | def handle_event(event, state) do 19 | IO.puts("Signals: Unhandled signal: #{inspect event}") 20 | {:ok, state} 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /livebooks/.gitignore: -------------------------------------------------------------------------------- 1 | data 2 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20210505210224_add_monitors.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.AddMonitors do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:monitors, primary_key: false) do 6 | add :logical_name, :string, primary_key: true 7 | add :name, :string 8 | add :last_analysis_run_at, :timestamp 9 | add :last_analysis_run_by, :string 10 | 11 | timestamps() 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20210506192225_add_instances.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.AddInstances do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:instances, primary_key: false) do 6 | add :name, :string, primary_key: true 7 | 8 | timestamps() 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20210512194547_create-invites.exs: -------------------------------------------------------------------------------- 1 | defmodule :"Elixir.Backend.DbpaRepo.Migrations.Create-invites" do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:invites, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :invitee_id, :string 8 | add :inviter_id, :string 9 | 10 | timestamps() 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20210517204132_create_snapshots.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.CreateSnapshots do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:snapshots, primary_key: false) do 6 | add :name, :string, primary_key: true 7 | add :data, :jsonb 8 | 9 | timestamps() 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20210518203159_create_monitor_checks.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.CreateMonitorChecks do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:monitor_checks, primary_key: false) do 6 | add :logical_name, :string, primary_key: true 7 | add :monitor_logical_name, :string, primary_key: true 8 | add :name, :string 9 | add :is_private, :boolean, default: false 10 | 11 | timestamps() 12 | end 13 | 14 | create index(:monitor_checks, [:monitor_logical_name]) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20210518214837_create_monitor_configs.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.CreateMonitorConfigs do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:monitor_configs, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :monitor_logical_name, :string 8 | add :check_logical_name, :string 9 | add :function_name, :string 10 | add :interval_secs, :integer 11 | add :extra_config, :jsonb 12 | add :run_groups, :jsonb 13 | 14 | timestamps() 15 | end 16 | create unique_index(:monitor_configs, [:monitor_logical_name, :check_logical_name]) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20210518224502_create_analyzer_configs.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.CreateAnalyzerConfigs do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:analyzer_configs, primary_key: false) do 6 | add :monitor_logical_name, :string, primary_key: true 7 | add :default_degraded_threshold, :float 8 | add :instances, :jsonb 9 | add :check_configs, :jsonb 10 | 11 | timestamps() 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20210519192855_create_monitor_errors.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.CreateMonitorErrors do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:monitor_errors, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :monitor_logical_name, :string 8 | add :check_logical_name, :string 9 | add :instance_name, :string 10 | add :message, :text 11 | add :time, :naive_datetime_usec 12 | 13 | timestamps() 14 | end 15 | 16 | create index(:monitor_errors, [:time]) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20210519194035_create_monitor_events.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.CreateMonitorEvents do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:monitor_events, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :monitor_logical_name, :string 8 | add :check_logical_name, :string 9 | add :instance_name, :string 10 | add :state, :string 11 | add :message, :text 12 | add :start_time, :naive_datetime_usec 13 | add :end_time, :naive_datetime_usec 14 | 15 | timestamps() 16 | end 17 | 18 | create index(:monitor_events, [:end_time]) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20210519230516_create_monitor_instances.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.CreateMonitorInstances do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:monitor_instances, primary_key: false) do 6 | add :monitor_logical_name, :string, primary_key: true 7 | add :instance_name, :string, primary_key: true 8 | add :last_report, :naive_datetime_usec 9 | add :check_last_reports, :jsonb 10 | 11 | timestamps() 12 | end 13 | 14 | create index(:monitor_instances, [:monitor_logical_name]) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20210617203216_create_subscriptions.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.CreateSubscriptions do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:subscriptions, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :monitor_id, :string 8 | add :delivery_method, :string 9 | add :identity, :string 10 | add :regions, {:array, :string} 11 | add :extra_config, :jsonb 12 | end 13 | 14 | create index(:subscriptions, [:delivery_method, :monitor_id]) 15 | create index(:subscriptions, [:delivery_method, :identity]) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20210622154817_add_status_page.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddStatusPage do 2 | use Ecto.Migration 3 | 4 | @moduledoc """ 5 | Tables for status pages. Note that we add this to DBPA: currently we'll only 6 | have SHARED, but this is an easy way to later on add private status page observations 7 | if we want to. Also meshes well with keeping all shared data in that special DBPA. 8 | """ 9 | 10 | def change do 11 | 12 | create table(:status_pages, primary_key: false) do 13 | add :id, :string, primary_key: true 14 | add :name, :string 15 | end 16 | 17 | create unique_index(:status_pages, [:name]) 18 | 19 | create table(:status_page_component_changes, primary_key: false) do 20 | add :id, :string, primary_key: true 21 | add :status_page_id, references(:status_pages, type: :string) 22 | add :component_name, :string 23 | add :status, :string 24 | add :instance, :string 25 | add :changed_at, :naive_datetime_usec 26 | end 27 | 28 | create index(:status_page_component_changes, [:status_page_id, :changed_at]) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20210623151924_create_monitor_tags.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.CreateMonitorTags do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:monitor_tags, primary_key: false) do 6 | add :monitor_logical_name, :string, primary_key: true 7 | add :tags, {:array, :string} 8 | end 9 | 10 | create index(:monitor_tags, [:tags], using: "GIN") 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20210629173209_create_alerts.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.CreateAlerts do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:alerts, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :monitor_logical_name, :string 8 | add :state, :string 9 | add :is_instance_specific, :boolean 10 | add :subscription_id, :string 11 | add :formatted_messages, :jsonb 12 | add :affected_regions, {:array, :string} 13 | add :affected_checks, {:array, :jsonb} 14 | add :generated_at, :naive_datetime_usec 15 | 16 | timestamps() 17 | end 18 | 19 | create index(:alerts, [:monitor_logical_name, :state, :generated_at]) 20 | create index(:alerts, [:state, :generated_at]) 21 | create index(:alerts, [:generated_at]) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20210629173846_create_alert_deliveries.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.CreateAlertDeliveries do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:alert_deliveries, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :alert_id, :string 8 | add :delivery_method, :string 9 | add :delivered_by_region, :string 10 | add :started_at, :naive_datetime_usec 11 | add :completed_at, :naive_datetime_usec 12 | 13 | timestamps() 14 | end 15 | 16 | create index(:alert_deliveries, [:delivery_method, :alert_id]) 17 | create index(:alert_deliveries, [:alert_id]) 18 | create index(:alert_deliveries, [:started_at]) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20210707174251_add_accepted_at_to_invites.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.AddAcceptedAtToInvites do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table (:invites) do 6 | add :accepted_at, :naive_datetime_usec 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20210722175324_create_subscription_deliveries.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.CreateSubscriptionDeliveries do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:subscription_deliveries, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :monitor_logical_name, :string 8 | add :alert_id, :string 9 | add :subscription_id, :string 10 | add :result, :text 11 | add :status_code, :integer 12 | 13 | timestamps() 14 | end 15 | 16 | create index(:subscription_deliveries, [:alert_id]) 17 | create index(:subscription_deliveries, [:monitor_logical_name]) 18 | create index(:subscription_deliveries, [:subscription_id]) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20210722175355_add_display_name_to_subscriptions.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.AddDisplayNameToSubscriptions do 2 | use Ecto.Migration 3 | alias Ecto.Migration.Runner 4 | 5 | def change do 6 | alter table (:subscriptions) do 7 | add :display_name, :string 8 | end 9 | 10 | # Default this to whatever is in the identity column. 11 | execute(&execute_up/0, &execute_down/0) 12 | end 13 | 14 | defp execute_up, do: repo().query!("update \"#{Runner.prefix()}\".subscriptions set display_name=identity where display_name IS NULL") 15 | defp execute_down, do: repo().query!("") 16 | end 17 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20210726225237_update_subscription_deliveries_indexes.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.UpdateSubscriptionDeliveriesIndexes do 2 | use Ecto.Migration 3 | 4 | def change do 5 | drop_if_exists index(:subscription_deliveries, [:monitor_logical_name]) 6 | create index(:subscription_deliveries, [:monitor_logical_name, :inserted_at]) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20210726233551_update_subscription_deliveries_to_denormalize_data.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.UpdateSubscriptionDeliveriesToDenormalizeData do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:subscription_deliveries) do 6 | add :delivery_method, :string 7 | add :display_name, :string 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20210806141833_add_snapshot_correlation_id.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddSnapshotCorrelationId do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table ("monitor_events") do 6 | add :correlation_id, :string 7 | end 8 | 9 | alter table ("alerts") do 10 | add :correlation_id, :string 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20210901200652_add_run_scenario_fields_to_monitor_config.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddRunScenarioFieldsToMonitorConfig do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("monitor_configs") do 6 | add :run_spec, :jsonb 7 | add :steps, :jsonb 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20211020144204_create_visible_monitors.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.CreateVisibleMonitors do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:visible_monitors, primary_key: false) do 6 | add :monitor_logical_name, :string, primary_key: true 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20220110155051_add_analyzer_config_new_defaults.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.AddAnalyzerConfigNewDefaults do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("analyzer_configs") do 6 | add :default_degraded_down_count, :integer 7 | add :default_degraded_up_count, :integer 8 | add :default_degraded_timeout, :integer 9 | add :default_error_timeout, :integer 10 | add :default_error_down_count, :integer 11 | add :default_error_up_count, :integer 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20220302233844_add_correlation_id_index_to_monitor_events.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddCorrelationIdIndexToMonitorEvents do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create index(:monitor_events, [:correlation_id, :inserted_at]) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20220321232244_add_monitor_events_logical_name_inserted_at_index.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.TelemetryWriteRepo.Migrations.AddMonitorEventsLogicalNameInsertedAtIndex do 2 | use Ecto.Migration 3 | 4 | def change do 5 | drop_if_exists index(:monitor_events, [:monitor_logical_name, :inserted_at]) 6 | create index(:monitor_events, [:monitor_logical_name, :start_time, :end_time]) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20220324153245_add_monitor_twitter_info.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddMonitorTwitterInfo do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:monitor_twitter_info, primary_key: false) do 6 | add :monitor_logical_name, :string, primary_key: true 7 | add :hashtags, {:array, :string} 8 | end 9 | 10 | create table(:monitor_twitter_counts, primary_key: false) do 11 | add :monitor_logical_name, :string, primary_key: true 12 | add :hashtag, :string, primary_key: true 13 | add :bucket_end_time, :naive_datetime_usec, primary_key: true 14 | add :bucket_duration, :integer 15 | add :count, :integer 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20220325194248_add_state_to_status_page_changes.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.AddStateToStatusPageChanges do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table (:status_page_component_changes) do 6 | add :state, :string 7 | end 8 | create index(:status_page_component_changes, [:state]) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20220509150804_add_blocked_steps_to_monitor_errors.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddBlockedStepsToMonitorErrors do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:monitor_errors) do 6 | add :blocked_steps, {:array, :string} 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20220526184853_drop_snapshot_table.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.DropSnapshotTable do 2 | use Ecto.Migration 3 | 4 | def change do 5 | drop table(:snapshots) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20220701181956_create_monitor_toggled_events.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.CreateMonitorToggledEvents do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:monitor_toggled_events, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :monitor_logical_name, :string 8 | add :state, :string 9 | 10 | timestamps() 11 | end 12 | create index(:monitor_toggled_events, [:monitor_logical_name]) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20220707225725_add_status_page_subscription.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.AddStatusPageSubscriptions do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table("status_page_subscriptions", primary_key: false) do 6 | add(:id, :string, primary_key: true) 7 | add(:status_page_id, :string) 8 | add(:component_id, :string) 9 | 10 | timestamps() 11 | end 12 | 13 | create(index("status_page_subscriptions", [:status_page_id, :component_id], unique: true)) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20220715194955_add_status_page_components.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.AddStatusPageComponents do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:status_page_components, primary_key: false) do 6 | add(:id, :string, primary_key: true) 7 | add(:status_page_id, :string) 8 | add(:name, :string) 9 | add(:exists, :boolean) 10 | add(:recent_change_id, :string) 11 | 12 | timestamps() 13 | end 14 | 15 | create(index(:status_page_components, [:status_page_id, :recent_change_id])) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20220715211439_add_maintenance_mode_flag_to_monitor.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddMaintenanceModeFlagToMonitor do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table (:monitors) do 6 | add :in_maintenance, :boolean, default: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20220812212658_remove_exists_from_status_page_components.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.RemoveExistsFromStatusPageComponents do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:status_page_components) do 6 | remove :exists 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20221102202557_add_keyset_pagination_index_for_monitor_errors.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.AddKeysetPaginationIndexForMonitorErrors do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create index(:monitor_errors, [:time, :id]) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20221104212658_remove_monitor_config_fields.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.RemoveMonitorConfigFields do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:monitor_configs) do 6 | remove :check_logical_name 7 | remove :function_name 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20221109175355_add_inserted_at_to_subscriptions.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.AddInsertedAtToSubscriptions do 2 | use Ecto.Migration 3 | alias Ecto.Migration.Runner 4 | 5 | def change do 6 | alter table (:subscriptions) do 7 | timestamps([default: NaiveDateTime.utc_now() |> to_string]) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20221115172338_add_monitor_name_to_alerts.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.AddMonitorNameToAlerts do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table ("alerts") do 6 | add :monitor_name, :string 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20221201173300_update_monitor_error_index.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.UpdateMonitorErrorIndex do 2 | use Ecto.Migration 3 | 4 | def change do 5 | drop_if_exists index(:monitor_errors, [:time]) 6 | create index(:monitor_errors, [:time, :monitor_logical_name]) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20221206141833_add_is_valid.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddIsValidColumn do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table ("monitor_events") do 6 | add :is_valid, :boolean, default: true 7 | end 8 | alter table ("monitor_errors") do 9 | add :is_valid, :boolean, default: true 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20230106181040_add_keyset_pagination_index_for_status_page_changes.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.AddKeysetPaginationIndexForStatusPageChanges do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create index(:status_page_component_changes, [:changed_at, :id]) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20230213191149_add_issue.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddIssue do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:issues, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :source, :string 8 | add :worst_state, :string 9 | add :service, :string 10 | add :start_time, :naive_datetime_usec 11 | add :end_time, :naive_datetime_usec 12 | timestamps() 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20230215172221_add_issue_events.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddIssueEvents do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:issue_events, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :issue_id, references(:issues, type: :string) 8 | add :source, :string 9 | add :source_id, :string 10 | add :check_logical_name, :string 11 | add :component_id, :string 12 | add :state, :string 13 | add :region, :string 14 | add :start_time, :naive_datetime_usec 15 | timestamps() 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20230301190504_add_index_to_issue_and_issue_events.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.AddIndexToIssueAndIssueEvents do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create index(:issues, [:service]) 6 | create index(:issues, [:start_time]) 7 | create index(:issues, [:end_time]) 8 | create index(:issues, [:start_time, :id]) 9 | 10 | create index(:issue_events, [:start_time]) 11 | create index(:issue_events, [:issue_id]) 12 | create index(:issue_events, [:start_time, :id]) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20230322205039_drop_monitors_in_maintenance.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.DropMonitorsInMaintenance do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table (:monitors) do 6 | remove :in_maintenance, :boolean, default: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20230322215310_drop_monitor_toggled_events.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.DropMonitorToggledEvents do 2 | use Ecto.Migration 3 | 4 | def change do 5 | drop table(:monitor_toggled_events) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20230424141559_add_issues_sources_field.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.AddIssuesSourcesField do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:issues) do 6 | add :sources, {:array, :string}, default: [] 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/dbpa_repo/migrations/20230424182554_remove_issues_source_field.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.DbpaRepo.Migrations.RemoveIssuesSourceField do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table (:issues) do 6 | remove :source 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto_sql], 3 | inputs: ["*.exs"] 4 | ] 5 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210504140159_create_users.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.CreateUsers do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:users, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :account_id, :string 8 | add :email, :string 9 | 10 | timestamps() 11 | end 12 | 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210504142318_create_projection_versions.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.CreateProjectionVersions do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:projection_versions, primary_key: false) do 6 | add(:projection_name, :text, primary_key: true) 7 | add(:last_seen_event_number, :bigint) 8 | timestamps(type: :naive_datetime_usec) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210504143407_add_users_by_email.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddUsersByEmail do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create index(:users, [:email]) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210504201151_add_user_uid.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddUserUid do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("users") do 6 | add :uid, :string 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210506190919_add_accounts_table.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddAccountsTable do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:accounts, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :name, :string 8 | 9 | timestamps() 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210510000950_create_keys.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.CreateKeys do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:keys, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :key_id, :string 8 | add :scheme, :string 9 | add :owner_type, :string 10 | add :owner_id, :string 11 | add :is_default, :boolean, default: false, null: false 12 | add :key, :string 13 | 14 | timestamps() 15 | end 16 | create unique_index("keys", [:owner_type, :owner_id, :is_default]) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210512000356_add_user_is_admin.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddUserIsAdmin do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("users") do 6 | add :is_admin, :boolean, default: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210514140939_add_slack_workspaces.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddSlackWorkspaces do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:slack_workspaces, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :account_id, :string 8 | add :team_name, :string 9 | add :scope, {:array, :string} 10 | add :bot_user_id, :string 11 | add :access_token, :string 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210518194512_add_internal_flag_to_accounts.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddInternalFlagToAccounts do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("accounts") do 6 | add :is_internal, :boolean, default: false 7 | end 8 | 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210519151754_create_api_tokens.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddApiTokens do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:api_tokens, primary_key: false) do 6 | add :api_token, :string, primary_key: true 7 | add :account_id, :string 8 | 9 | timestamps() 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210525193500_create_slack_slash_commands.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.CreateSlackSlashCommands do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:slack_slash_commands, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :data, :jsonb 8 | 9 | timestamps() 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210525211719_create_microsoft_teams_commands.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.CreateMicrosoftTeamsCommands do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:microsoft_teams_commands, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :data, :jsonb 8 | 9 | timestamps() 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210526174559_create_microsoft_tenants.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.CreateMicrosoftTenants do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:microsoft_tenants, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :account_id, :string 8 | add :team_id, :string 9 | add :team_name, :string 10 | add :service_url, :string 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210527151303_create_teams_workspaces.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.CreateTeamsWorkspaces do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:teams_workspaces, primary_key: false) do 6 | add :tenant_uuid, :string, primary_key: true 7 | add :account_id, :string 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210607183443_drop-teams-workspaces.exs: -------------------------------------------------------------------------------- 1 | defmodule :"Elixir.Backend.Repo.Migrations.Drop-teams-workspaces" do 2 | use Ecto.Migration 3 | 4 | def change do 5 | drop table(:teams_workspaces) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210813174912_create_notices.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.CreateNotices do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:notices, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :monitor_id, :string 8 | add :summary, :string 9 | add :description, :text 10 | add :end_date, :naive_datetime 11 | 12 | timestamps() 13 | end 14 | 15 | create index(:notices, [:monitor_id]) 16 | create index(:notices, [:end_date]) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210922224433_add_microsoft_tenant_name.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddMicrosoftTenantName do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table ("microsoft_tenants") do 6 | add :name, :string 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20210924161702_add_user_hubspot_contact_id.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddHubspotContacts do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("users") do 6 | add :hubspot_contact_id, :string 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20211108205221_add_last_login.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddLastLogin do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:users) do 6 | add :last_login, :naive_datetime_usec 7 | end 8 | 9 | create index(:users, [:last_login]) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /priv/repo/migrations/20211116201505_add_web_login_aggregate.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddWebLoginAggregate do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:aggregate_web_login, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :time, :naive_datetime_usec 8 | add :user_id, :string 9 | add :is_internal, :boolean 10 | end 11 | create index(:aggregate_web_login, [:time, :is_internal, :user_id]) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/repo/migrations/20211116201933_add_app_use_aggregate.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddAppUseAggregate do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:aggregate_app_use, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :time, :naive_datetime_usec 8 | add :user_id, :string 9 | add :app_type, :string 10 | add :is_internal, :boolean 11 | end 12 | create index(:aggregate_app_use, [:time, :is_internal, :app_type, :user_id]) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /priv/repo/migrations/20211123005705_add_webhooks.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddWebhooks do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:webhooks, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :monitor_logical_name, :string 8 | add :instance_name, :string 9 | add :data, :text 10 | add :content_type, :string 11 | 12 | timestamps(type: :naive_datetime_usec) 13 | end 14 | create index(:webhooks, [:inserted_at, :monitor_logical_name, :instance_name]) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /priv/repo/migrations/20211202154419_add_notice_reads.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddNoticeReads do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table("notice_reads", primary_key: false) do 6 | add :notice_id, references(:notices, type: :string) 7 | add :user_id, references(:users, type: :string) 8 | timestamps() 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220318175952_add_user_timezone.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddUserTimezone do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("users") do 6 | add :timezone, :string 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220502142526_add_api_counts.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddApiCounts do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:aggregate_api_use, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :time, :naive_datetime_usec 8 | add :account_id, :string 9 | add :is_internal, :boolean 10 | end 11 | create index(:aggregate_api_use, [:time, :is_internal, :account_id]) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220506150315_add_flow_aggregate.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddFlowAggregate do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:aggregate_flow, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :name, :string 8 | add :last_step, :string 9 | 10 | timestamps() 11 | end 12 | # We usually will display records from a single type of flow started 13 | # during a certain time period. 14 | create index(:aggregate_flow, [:name, :inserted_at]) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220525164557_add_account_analytics_columns.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddAccountAnalyticsColumns do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:accounts) do 6 | add :stat_num_subscriptions, :integer, default: 0 7 | add :stat_num_monitors, :integer, default: 0 8 | add :stat_num_users, :integer, default: 0 9 | add :stat_last_user_login, :naive_datetime 10 | add :stat_num_msteams, :integer, default: 0 11 | add :stat_num_slack, :integer, default: 0 12 | add :stat_weekly_users, :integer, default: 0 13 | add :stat_monthly_users, :integer, default: 0 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220602001155_add_num_slack_alerts_to_account_analytics_columns.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddNumSlackAlertsToAccountAnalyticsColumns do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:accounts) do 6 | add :stat_num_slack_alerts, :integer, default: 0 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220609160333_add_num_slack_commands_to_account.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddNumSlackCommandsToAccount do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:accounts) do 6 | add :stat_num_slack_commands, :integer, default: 0 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220614201813_add_last_webapp_activity_to_account.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddLastWebappActivityToAccount do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:accounts) do 6 | add :stat_last_webapp_activity, :naive_datetime 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220615172726_add_account_free_trial_end_time.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddAccountFreeTrialEndTime do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("accounts") do 6 | add :free_trial_end_time, :naive_datetime 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220616210321_add_stripe_customer_id.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddStripeCustomerId do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:accounts) do 6 | add :stripe_customer_id, :string 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220620162152_create_account_memberships.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.CreateAccountMemberships do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:memberships, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :account_id, :string 8 | add :tier, :string 9 | add :billing_period, :string 10 | add :start_date, :naive_datetime 11 | add :end_date, :naive_datetime 12 | 13 | timestamps() 14 | end 15 | 16 | create index(:memberships, [:account_id, :end_date]) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220624191342_rename_is_admin_to_is_metrist_admin.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.RenameIsAdminToIsMetristAdmin do 2 | use Ecto.Migration 3 | 4 | def change do 5 | rename table("users"), :is_admin, to: :is_metrist_admin 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220704134912_add_user_is_read_only.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddUserIsReadOnly do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("users") do 6 | add :is_read_only, :boolean, default: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220714180902_add_original_user_id.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddOriginalUserId do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:accounts) do 6 | add :original_user_id, :string 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220714181824_add_user_last_seen_slack_team_id.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddUserLastSeenSlackTeamId do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("users") do 6 | add :last_seen_slack_team_id, :string 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220714193749_add_user_last_seen_slack_user_id.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddUserLastSeenSlackUserId do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table("users") do 6 | add :last_seen_slack_user_id, :string 7 | end 8 | create index(:users, [:last_seen_slack_user_id]) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220912193445_add_new_signups.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddNewSignups do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:aggregate_new_signups, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :time, :naive_datetime_usec 8 | end 9 | create index(:aggregate_new_signups, [:time]) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /priv/repo/migrations/20221115214049_add_retry_process.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddRetryProcess do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:notification_retry_process, primary_key: false) do 6 | add :id, :string, primary_key: true 7 | 8 | timestamps() 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /priv/repo/migrations/20230328163042_add_datadog_access_grants_table.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.AddDatadogAccessGrants do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table("datadog_access_grants", primary_key: false) do 6 | add :id, :string, primary_key: true 7 | add :verifier, :string 8 | add :access_token, :string 9 | add :refresh_token, :string 10 | add :user_id, references(:users, type: :string) 11 | add :scope, {:array, :string} 12 | add :expires_in, :integer 13 | add :expires_at, :utc_datetime 14 | 15 | timestamps() 16 | end 17 | create index("datadog_access_grants", [:user_id], unique: true) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # Backend.Repo.insert!(%Backend.SomeSchema{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | -------------------------------------------------------------------------------- /priv/slack-completion.md: -------------------------------------------------------------------------------- 1 | Next, let's set up notifications. We will send notifications when a dependency's status changes. 2 | 3 | To send notifications to a channel, create a subscription by running this command in Slack: `/metrist subscriptions ` 4 | 5 | ![Slack subscriptions example](/images/slack-install-subscriptions.svg) 6 | 7 | Replace #<channel-name> with your preferred Slack channel. 8 | 9 | Click the “Select your monitors” button to manage which channels will receive status change notifications. 10 | 11 | ![Slack choose monitors example](/images/slack-install-choose-monitors.svg) 12 | 13 | Run `/metrist help` to view additional Metrist Slack commands. 14 | -------------------------------------------------------------------------------- /priv/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metrist-Software/backend/3606d173f0b5647156ba19697889d48250337de1/priv/static/favicon.ico -------------------------------------------------------------------------------- /priv/static/images/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metrist-Software/backend/3606d173f0b5647156ba19697889d48250337de1/priv/static/images/default.png -------------------------------------------------------------------------------- /priv/static/images/icon-reports.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /priv/static/images/phoenix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metrist-Software/backend/3606d173f0b5647156ba19697889d48250337de1/priv/static/images/phoenix.png -------------------------------------------------------------------------------- /priv/static/images/providers/azure-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /priv/static/images/providers/dark-azure-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /priv/static/images/providers/dark-default-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /priv/static/images/providers/dark-gcp-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /priv/static/images/providers/default-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /priv/static/images/providers/gcp-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /priv/static/images/teams-install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metrist-Software/backend/3606d173f0b5647156ba19697889d48250337de1/priv/static/images/teams-install.png -------------------------------------------------------------------------------- /priv/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /priv/static/svg/brand/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /priv/static/svg/brand/logo-combined-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /priv/static/svg/generic/azure-icon-dark.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /priv/static/svg/generic/azure-icon.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /priv/static/svg/generic/chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /priv/static/svg/generic/chevron-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/static/svg/generic/copy-to-clipboard-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/static/svg/generic/gcp-icon-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /priv/static/svg/generic/gcp-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /priv/static/svg/generic/icon-book.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/static/svg/generic/icon-calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/static/svg/generic/icon-check-mark-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /priv/static/svg/generic/icon-check-mark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/static/svg/generic/icon-close-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/static/svg/generic/icon-close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/static/svg/generic/icon-dark-mode.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /priv/static/svg/generic/icon-download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /priv/static/svg/generic/icon-envelope.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/static/svg/generic/icon-feedback.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/static/svg/generic/icon-filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/static/svg/generic/icon-info-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/static/svg/generic/icon-info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/static/svg/generic/icon-plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/static/svg/generic/icon-profile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/static/svg/generic/icon-solid-selector.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /priv/static/svg/generic/icon-trash.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /priv/static/svg/generic/icon-x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/static/svg/login/background-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/static/svg/login/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/static/svg/login/google.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /priv/static/svg/login/microsoft.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /priv/static/svg/login/signup-message.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /priv/static/svg/monitors/icon-degraded.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /priv/static/svg/monitors/icon-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /priv/static/svg/monitors/icon-functional-testing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/static/svg/monitors/icon-in-app.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/teams-install.md: -------------------------------------------------------------------------------- 1 | 1. Download our Microsoft Teams app zip from [here](/distributions/%2Fapps%2F) 2 | 2. Save the .zip file provided to a location on your computer 3 | 3. If required, log into Microsoft Teams where you intend to install the application 4 | 4. On the left bar, find **Apps** right above the **Help** icon in the bottom left 5 | You must be the team owner, or the owner must allow users to add the appropriate app types for this functionality to appear. 6 | 5. Choose **Upload a custom app** on the lower right. 7 | ![Teams-Install-Image](/images/teams-install.png) 8 | 6. Browse to and select your .zip package from your computer. 9 | 7. After a brief pause you will see the Metrist application in the list. 10 | 8. Click on **Metrist** and beside the **Open** button click the down arrow 11 | 9. Choose **Add to a Team** and choose the team you would like to install the app within 12 | 13 | Your Microsoft Teams installation needs to have the appropriate custom app settings to allow the installation of a custom application via a zip file. Details on custom app policies can be found [here](https://docs.microsoft.com/en-us/microsoftteams/teams-custom-app-policies-and-settings). 14 | -------------------------------------------------------------------------------- /priv/telemetry_repo/migrations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metrist-Software/backend/3606d173f0b5647156ba19697889d48250337de1/priv/telemetry_repo/migrations/.gitkeep -------------------------------------------------------------------------------- /priv/telemetry_write_repo/migrations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metrist-Software/backend/3606d173f0b5647156ba19697889d48250337de1/priv/telemetry_write_repo/migrations/.gitkeep -------------------------------------------------------------------------------- /priv/telemetry_write_repo/migrations/20210726200412_create_projection_versions.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Repo.Migrations.CreateProjectionVersions do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:projection_versions, primary_key: false) do 6 | add(:projection_name, :text, primary_key: true) 7 | add(:last_seen_event_number, :bigint) 8 | timestamps(type: :naive_datetime_usec) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /priv/telemetry_write_repo/migrations/20211215143920_set_retention.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.TelemetryWriteRepo.Migrations.SetRetention do 2 | use Ecto.Migration 3 | 4 | def up do 5 | execute "SELECT add_retention_policy('monitor_telemetry', INTERVAL '90 days')" 6 | end 7 | 8 | def down do 9 | execute "SELECT remove_retention_policy('monitor_telemetry')" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /rel/env.sh.eex: -------------------------------------------------------------------------------- 1 | 2 | # Sets and enables heart (recommended only in daemon mode) 3 | # case $RELEASE_COMMAND in 4 | # daemon*) 5 | # HEART_COMMAND="$RELEASE_ROOT/bin/$RELEASE_NAME $RELEASE_COMMAND" 6 | # export HEART_COMMAND 7 | # export ELIXIR_ERL_OPTIONS="-heart" 8 | # ;; 9 | # *) 10 | # ;; 11 | # esac 12 | 13 | # Set the release to work across nodes. If using the long name format like 14 | # the one below (my_app@127.0.0.1), you need to also uncomment the 15 | # RELEASE_DISTRIBUTION variable below. Must be "sname", "name" or "none". 16 | # export RELEASE_DISTRIBUTION=name 17 | # export RELEASE_NODE=<%= @release.name %>@127.0.0.1 18 | 19 | # Cookies are not a security mechanism, or at best a very weak one. We 20 | # just use it to make sure that "wrong" nodes don't accidentally connect. 21 | export RELEASE_COOKIE=backend-node 22 | 23 | export HOSTNAME=`curl http://169.254.169.254/latest/meta-data/local-ipv4` 24 | export RELEASE_NODE="app@${HOSTNAME}" 25 | export RELEASE_DISTRIBUTION="name" 26 | cat < String.to_atom() 8 | {:ok, _pid} = Backend.MonitorErrorTelemetry.start_link([], name) 9 | make_error = fn monitor_id -> 10 | %Domain.Monitor.Commands.AddError{ 11 | id: monitor_id, 12 | error_id: "ignored", 13 | check_logical_name: "ignored", 14 | instance_name: "ignored", 15 | message: "ignored", 16 | report_time: "ignored", 17 | is_private: false 18 | } 19 | end 20 | 21 | MonitorErrorTelemetry.register_error(make_error.("SHARED_one"), name) 22 | MonitorErrorTelemetry.register_error(make_error.("SHARED_one"), name) 23 | MonitorErrorTelemetry.register_error(make_error.("SHARED_two"), name) 24 | 25 | # Counts are correct 26 | counts = GenServer.call(name, :errors) 27 | assert counts == %{"SHARED_one" => 2, "SHARED_two" => 1} 28 | 29 | # Counts are reset after a call. 30 | counts = GenServer.call(name, :errors) 31 | assert counts == %{} 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/backend/projections/dbpa/monitor_config_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Projections.Dbpa.MonitorConfigTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Backend.Projections.Dbpa.MonitorConfig 5 | alias Domain.Monitor.Commands.RunSpec 6 | alias Domain.Monitor.Commands.Step 7 | 8 | test "atomification" do 9 | db_result = %MonitorConfig{ 10 | steps: [ 11 | %{"check_logical_name" => "Check", "timeout_secs" => 0} 12 | ], 13 | run_spec: %{ 14 | "run_type" => "dll", 15 | "name" => "my_name" 16 | } 17 | } 18 | 19 | expected = %MonitorConfig{ 20 | steps: [ 21 | %Step{check_logical_name: "Check", timeout_secs: 0} 22 | ], 23 | run_spec: %RunSpec{ 24 | run_type: :dll, 25 | name: "my_name" 26 | } 27 | } 28 | 29 | assert expected == MonitorConfig.deserialize_inner_json(db_result) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/backend/projectors/ecto.ex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metrist-Software/backend/3606d173f0b5647156ba19697889d48250337de1/test/backend/projectors/ecto.ex -------------------------------------------------------------------------------- /test/backend/projectors/ecto_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.Projectors.EctoTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Backend.Projectors.Ecto, only: [dt_with_default_from_meta: 2] 5 | 6 | # Pretty much more documentation than a "proper" test to 7 | # stress that event metadata has regular datetimes and 8 | # we work mostly wth naive datetimes. 9 | test "date with default" do 10 | meta_dt = ~U[2022-04-08 18:13:52.729262Z] 11 | meta = Support.CommandedHelpers.fake_metadata(%{created_at: meta_dt}) 12 | now = NaiveDateTime.utc_now() 13 | meta_ndt = DateTime.to_naive(meta_dt) 14 | 15 | assert meta_ndt == dt_with_default_from_meta(nil, meta) 16 | assert now == dt_with_default_from_meta(now, meta) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/backend/statuspages/aws_status_page_scraper_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.StatusPages.AwsStatusPageScraperTest do 2 | use ExUnit.Case, async: true 3 | require Logger 4 | 5 | alias Backend.StatusPages.AwsStatusPageScraper 6 | 7 | test "Can parse aws sample RSS into components" do 8 | component = File.read!("test/backend/test_data/ec2-us-east-1.rss") 9 | |> AwsStatusPageScraper.process_rss_body("us-east-1", "ec2") 10 | 11 | assert {"ec2", "Amazon Elastic Compute Cloud (N. Virginia) Service Status","us-east-1", "Good"} == component 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/backend/statuspages/azure_dev_ops_status_page_scraper_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.StatusPages.AzureDevOpsStatusPageScraperTest do 2 | use ExUnit.Case, async: true 3 | require Logger 4 | 5 | alias Backend.StatusPages.AzureDevOpsStatusPageScraper 6 | 7 | test "Can parse azure dev ops sample into components" do 8 | {:ok, observations} = File.read!("test/backend/test_data/azure_dev_ops_status_page_example.html") 9 | |> AzureDevOpsStatusPageScraper.process_body() 10 | 11 | {_key, devops_observations} = Enum.find(observations, fn {key, _obs} -> key == "azuredevopsboards" end) 12 | assert List.first(devops_observations).status == "Unhealthy" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/backend/statuspages/azure_status_page_scraper_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.StatusPages.AzureStatusPageScraperTest do 2 | use ExUnit.Case, async: true 3 | require Logger 4 | 5 | alias Backend.StatusPages.AzureStatusPageScraper 6 | 7 | test "Can parse new page into components" do 8 | body = File.read!("test/backend/test_data/azure_status_page_example.html") 9 | 10 | res = AzureStatusPageScraper.process_body(body) 11 | 12 | {_key, azure_observations} = Enum.find(res, fn {key, _obs} -> key == "azuremonitor" end) 13 | assert List.first(azure_observations).status == "Good" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/backend/statuspages/gcp_status_page_scraper_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Backend.StatusPages.GcpStatusPageScraperTest do 2 | use ExUnit.Case, async: true 3 | require Logger 4 | 5 | alias Backend.StatusPages.GcpStatusPageScraper 6 | 7 | test "Can parse gcp sample into components by service" do 8 | components = 9 | File.read!("test/backend/test_data/google_status_page_example.html") 10 | |> GcpStatusPageScraper.scrape_from_body_text() 11 | 12 | assert Enum.any?(components, fn {id, observations} -> 13 | id == "gke" && 14 | Enum.any?(observations, fn %Domain.StatusPage.Commands.Observation{ 15 | component: name, 16 | status: status, 17 | state: state 18 | } -> 19 | name == "Google Kubernetes Engine" && status == "disruption" && 20 | state == :degraded 21 | end) 22 | end) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/backend_web/api_helper_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.ApiHelperTest do 2 | use ExUnit.Case, async: true 3 | alias BackendWeb.ApiHelpers 4 | 5 | describe "get_daterange_from_params/1" do 6 | test "Always returns utc" do 7 | params = %{"from" => "2022-11-28T19:00:00+03:00", "to" => nil} 8 | {from, _to} = ApiHelpers.get_daterange_from_params(params) 9 | assert from == ~N[2022-11-28T16:00:00] 10 | end 11 | 12 | test "Handles nil values for from or to" do 13 | params = %{"from" => "2022-11-28T00:00:00Z", "to" => nil} 14 | {from, to} = ApiHelpers.get_daterange_from_params(params) 15 | assert is_nil(to) 16 | assert not is_nil(from) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/backend_web/components/list_group_select_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.MonitorLiveTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "Most recently checked is in the right order" do 5 | dates = [~N[2021-11-15 19:41:02.295887], ~N[2021-11-15 19:21:44.434548], ~N[2021-11-15 19:41:02.29588]] 6 | most_recent = BackendWeb.MonitorDetailLive.most_recent_date(dates) 7 | 8 | assert most_recent == Enum.at(dates, 0) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/backend_web/live/monitors_live_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.Components.ListGroupSelectTest do 2 | use ExUnit.Case, async: true 3 | alias BackendWeb.Components.ListGroupSelect 4 | 5 | describe "filter_groups/2" do 6 | @data [ 7 | %{ 8 | id: "group1", 9 | label: "Group1", 10 | children: [ 11 | %{ 12 | id: "child1", 13 | label: "Child1" 14 | }, 15 | %{ 16 | id: "child2", 17 | label: "Child2" 18 | }, 19 | %{ 20 | id: "child3", 21 | label: "Child3" 22 | } 23 | ] 24 | }, 25 | %{ 26 | id: "group1", 27 | label: "Group2", 28 | children: [] 29 | } 30 | ] 31 | 32 | test "filters monitors" do 33 | assert ListGroupSelect.filter_groups(@data, "child1") == [ 34 | %{children: [%{id: "child1", label: "Child1"}], id: "group1", label: "Group1"} 35 | ] 36 | end 37 | 38 | test "filters group" do 39 | assert ListGroupSelect.filter_groups(@data, "group1") == [%{children: [], id: "group1", label: "Group1"}] 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/domain/clock_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Domain.ClockTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Domain.Clock 5 | 6 | test "New clock just starts ticking" do 7 | assert %Clock.Ticked{id: "minute", value: 42} == Clock.execute(%Clock{}, %Clock.Tick{id: "minute", value: 42}) 8 | end 9 | 10 | test "New value emits a tick" do 11 | assert %Clock.Ticked{id: "minute", value: 43} == Clock.execute(%Clock{id: "minute", value: 42}, %Clock.Tick{id: "minute", value: 43}) 12 | end 13 | 14 | test "Duplicate ticks are discarded" do 15 | assert nil == Clock.execute(%Clock{id: "minute", value: 42}, %Clock.Tick{id: "minute", value: 42}) 16 | end 17 | 18 | test "Time only goes forward" do 19 | assert nil == Clock.execute(%Clock{id: "minute", value: 42}, %Clock.Tick{id: "minute", value: 41}) 20 | end 21 | 22 | test "New clock is correctly updated" do 23 | assert %Clock{id: "minute", value: 42} == Clock.apply(%Clock{}, %Clock.Ticked{id: "minute", value: 42}) 24 | end 25 | 26 | test "Existing clock is correctly updated" do 27 | assert %Clock{id: "minute", value: 43} == Clock.apply(%Clock{id: "minute", value: 42}, %Clock.Ticked{id: "minute", value: 43}) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/domain/notice_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Domain.NoticeTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "Can't process commands without a create" do 5 | user = %Domain.Notice{} 6 | cmd = %Domain.Notice.Commands.Clear{id: "42"} 7 | 8 | ExUnit.CaptureLog.capture_log(fn -> 9 | assert {:error, :no_create_command_seen} == Domain.Notice.execute(user, cmd) 10 | end) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/domain/tagging_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Domain.TaggingTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Domain.Tagging 5 | 6 | test "legacy vs standard tagging" do 7 | assert Tagging.is_legacy?("gcp") 8 | refute Tagging.is_legacy?("vendor:gcp") 9 | end 10 | 11 | test "validity checking" do 12 | assert Tagging.is_valid?("vendor:gcp") 13 | assert Tagging.is_valid?("metrist:group:subtype:value") 14 | refute Tagging.is_valid?("gcp") 15 | refute Tagging.is_valid?("thi/s:shouldnot:have:that:slash:in:the:key") 16 | end 17 | 18 | test "splitting into key and value pairs" do 19 | assert {"vendor", "gcp"} = Tagging.kv("vendor:gcp") 20 | assert {"my:sub:key", "value?123"} = Tagging.kv("my:sub:key:value?123") 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule BackendWeb.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | import other functionality to make it easier 8 | to build common data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | we enable the SQL sandbox, so changes done to the database 12 | are reverted at the end of every test. If you are using 13 | PostgreSQL, you can even run database tests asynchronously 14 | by setting `use BackendWeb.ChannelCase, async: true`, although 15 | this option is not recommended for other databases. 16 | """ 17 | 18 | use ExUnit.CaseTemplate 19 | 20 | using do 21 | quote do 22 | # Import conveniences for testing with channels 23 | import Phoenix.ChannelTest 24 | import BackendWeb.ChannelCase 25 | 26 | # The default endpoint for testing 27 | @endpoint BackendWeb.Endpoint 28 | end 29 | end 30 | 31 | setup tags do 32 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Backend.Repo) 33 | 34 | unless tags[:async] do 35 | Ecto.Adapters.SQL.Sandbox.mode(Backend.Repo, {:shared, self()}) 36 | end 37 | 38 | :ok 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/support/commanded_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule Support.CommandedHelpers do 2 | @doc """ 3 | For testing projectors and event handlers, properly formed fake metadata. You 4 | can override what you want in the arguments. 5 | """ 6 | def fake_metadata(opts \\ %{}) do 7 | %{ 8 | :application => Backend.App, 9 | :causation_id => UUID.uuid1(), 10 | :correlation_id => UUID.uuid1(), 11 | :created_at => DateTime.utc_now(), # And not NaiveDateTime! 12 | :event_id => UUID.uuid1(), 13 | :event_number => 42, 14 | :handler_name => "TestHandler", 15 | :state => nil, 16 | :stream_id => "SHARED_testsignal", 17 | :stream_version => 123, 18 | "actor" => %{ 19 | "kind" => "admin", 20 | "method" => "db_setup" 21 | } 22 | } 23 | |> Map.merge(opts) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/support/notifications/test_handlers.ex: -------------------------------------------------------------------------------- 1 | defmodule Test.Support.Notifications.TestHandlers do 2 | defmodule SuccessfulSingleResponse do 3 | def make_request(_, _), do: "this is a test" 4 | def get_response(_), do: {:ok, %{status_code: 200}} 5 | end 6 | 7 | defmodule ErrorSingleResponse do 8 | def make_request(_, _), do: "this is a test" 9 | def get_response(_), do: {:error, %{status_code: 200}} 10 | end 11 | 12 | defmodule SingleResponse do 13 | def make_request(_, _), do: "this is a test" 14 | def get_response(_), do: {:ok, %{status_code: 200}} 15 | end 16 | 17 | defmodule SuccessfulMultiResponse do 18 | def make_request(_, _), do: ["good", "good", "good"] 19 | def get_response(_), do: {:ok, %{status_code: 200}} 20 | end 21 | 22 | defmodule ErrorMultiResponse do 23 | def make_request(_, _), do: ["good", "bad", "good"] 24 | def get_response("good"), do: {:ok, %{status_code: 200}} 25 | def get_response("bad"), do: {:error, %{status_code: 200}} 26 | end 27 | 28 | defmodule NonStandardResponse do 29 | def make_request(_, _), do: "test" 30 | def get_response(_), do: {:ok, "Response"} 31 | def response_ok?({:ok, "Response"}), do: true 32 | def response_status_code({:ok, "Response"}), do: 201 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/support/test_crypt_repo.ex: -------------------------------------------------------------------------------- 1 | defmodule TestCryptRepo do 2 | @behaviour Domain.CryptRepo 3 | 4 | @scheme :aes_256_cbc 5 | 6 | @key { 7 | "test_key_id", 8 | "#{@scheme}", 9 | Domain.CryptUtils.gen_random(Domain.CryptUtils.key_bytes(@scheme)) 10 | } 11 | 12 | @impl true 13 | def initialize(), do: :ok 14 | 15 | @impl true 16 | def key_for(_owner_type, _owner_id) do 17 | @key 18 | end 19 | 20 | @impl true 21 | def get(id) do 22 | {_id, scheme, key} = @key 23 | {id, scheme, key} 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/support/test_struct.ex: -------------------------------------------------------------------------------- 1 | use TypedStruct 2 | 3 | typedstruct module: TestStruct do 4 | plugin(Backend.JsonUtils) 5 | 6 | field :id, String.t() 7 | field :timestamp, NaiveDateTime.t() 8 | end 9 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | #Ecto.Adapters.SQL.Sandbox.mode(Backend.Repo, :manual) 3 | -------------------------------------------------------------------------------- /wordpress/README.md: -------------------------------------------------------------------------------- 1 | # Wordpress files 2 | 3 | ## realtimedata.{css,js,html} 4 | 5 | Files required to render the realtime data section in our marketing website (metrist.io) 6 | 7 | The JavaScript file is stored under `s3://canary-public-assets/dist/js/` so you'll have to run the following 8 | if the file is modified 9 | 10 | ```sh 11 | aws s3 cp realtimedata.js s3://canary-public-assets/dist/js/ 12 | ``` 13 | 14 | The CSS needs to be copied manually to kinsta. Note that the `selector` CSS selector is a special selector 15 | only available in Kinsta to refer to the container element 16 | 17 | The HTML file is there for testing. You can run the following to serve the static files in http://localhost:9000 18 | 19 | ```sh 20 | python3 -m http.server 9000 21 | ``` 22 | 23 | -------------------------------------------------------------------------------- /wordpress/realtimedata.html: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 |
16 | 17 | 35 | 36 | 37 | --------------------------------------------------------------------------------