1: -module(service_mongoose_system_metrics_SUITE).
    2: 
    3: -compile([export_all, nowarn_export_all]).
    4: 
    5: -include_lib("common_test/include/ct.hrl").
    6: -include_lib("eunit/include/eunit.hrl").
    7: 
    8: -define(SERVER_URL, "http://localhost:8765").
    9: -define(ETS_TABLE, qs).
   10: -define(TRACKING_ID, #{id => "G-7KQE4W9SVJ", secret => "Secret"}).
   11: -define(TRACKING_ID_CI, #{id => "G-VB91V60SKT", secret => "Secret2"}).
   12: -define(TRACKING_ID_EXTRA, #{id => "UA-EXTRA-TRACKING-ID", secret => "Secret3"}).
   13: 
   14: -record(event, {
   15:     name = <<>>,
   16:     params = #{},
   17:     client_id = <<>>,
   18:     instance_id = <<>>,
   19:     app_secret = <<>>}).
   20: 
   21: -import(distributed_helper, [mim/0, mim2/0, mim3/0, rpc/4,
   22:                              require_rpc_nodes/1
   23:                             ]).
   24: 
   25: -import(component_helper, [connect_component/1,
   26:                            disconnect_component/2,
   27:                            get_components/1]).
   28: 
   29: -import(domain_helper, [host_type/0]).
   30: -import(config_parser_helper, [mod_config/2, config/2]).
   31: 
   32: suite() ->
   33:     require_rpc_nodes([mim]).
   34: 
   35: all() ->
   36:     [
   37:      system_metrics_are_not_reported_when_not_allowed,
   38:      periodic_report_available,
   39:      all_clustered_mongooses_report_the_same_client_id,
   40:      system_metrics_are_reported_to_google_analytics_when_mim_starts,
   41:      system_metrics_are_reported_to_configurable_google_analytics,
   42:      system_metrics_are_reported_to_a_json_file,
   43:      mongoose_version_is_reported,
   44:      cluster_uptime_is_reported,
   45:      xmpp_components_are_reported,
   46:      api_are_reported,
   47:      transport_mechanisms_are_reported,
   48:      outgoing_pools_are_reported,
   49:      xmpp_stanzas_counts_are_reported,
   50:      config_type_is_reported,
   51:      {group, module_opts},
   52:      {group, log_transparency}
   53:     ].
   54: 
   55: groups() ->
   56:     [
   57:      {module_opts, [], [
   58:                         module_opts_are_reported,
   59:                         rdbms_module_opts_are_reported
   60:                        ]},
   61:      {log_transparency, [], [
   62:                              just_removed_from_config_logs_question,
   63:                              in_config_unmodified_logs_request_for_agreement,
   64:                              in_config_with_explicit_no_report_goes_off_silently,
   65:                              in_config_with_explicit_reporting_goes_on_silently
   66:                             ]}
   67:     ].
   68: 
   69: -define(APPS, [inets, crypto, ssl, ranch, cowlib, cowboy]).
   70: 
   71: %%--------------------------------------------------------------------
   72: %% Suite configuration
   73: %%--------------------------------------------------------------------
   74: init_per_suite(Config) ->
   75:     [ {ok, _} = application:ensure_all_started(App) || App <- ?APPS ],
   76:     http_helper:start(8765, "/[...]", fun handler_init/1),
   77:     Config1 = escalus:init_per_suite(Config),
   78:     Config2 = dynamic_services:save_services([mim(), mim2()], Config1),
   79:     ejabberd_node_utils:init(Config2).
   80: 
   81: end_per_suite(Config) ->
   82:     http_helper:stop(),
   83:     dynamic_services:restore_services(Config),
   84:     escalus:end_per_suite(Config).
   85: 
   86: %%--------------------------------------------------------------------
   87: %% Init & teardown
   88: %%--------------------------------------------------------------------
   89: init_per_group(module_opts, Config) ->
   90:     dynamic_modules:save_modules(host_type(), Config);
   91: init_per_group(log_transparency, Config) ->
   92:     logger_ct_backend:start(),
   93:     logger_ct_backend:capture(warning),
   94:     Config;
   95: init_per_group(_GroupName, Config) ->
   96:     Config.
   97: 
   98: end_per_group(module_opts, Config) ->
   99:     dynamic_modules:restore_modules(Config);
  100: end_per_group(log_transparency, Config) ->
  101:     logger_ct_backend:stop_capture(),
  102:     Config;
  103: end_per_group(_GroupName, Config) ->
  104:     Config.
  105: 
  106: init_per_testcase(system_metrics_are_not_reported_when_not_allowed, Config) ->
  107:     create_events_collection(),
  108:     disable_system_metrics(mim()),
  109:     delete_prev_client_id(mim()),
  110:     Config;
  111: init_per_testcase(all_clustered_mongooses_report_the_same_client_id, Config) ->
  112:     create_events_collection(),
  113:     distributed_helper:add_node_to_cluster(mim2(), Config),
  114:     enable_system_metrics(mim()),
  115:     enable_system_metrics(mim2()),
  116:     Config;
  117: init_per_testcase(system_metrics_are_reported_to_configurable_google_analytics, Config) ->
  118:     create_events_collection(),
  119:     enable_system_metrics_with_configurable_tracking_id(mim()),
  120:     Config;
  121: init_per_testcase(xmpp_components_are_reported, Config) ->
  122:     create_events_collection(),
  123:     Config1 = get_components(Config),
  124:     enable_system_metrics(mim()),
  125:     Config1;
  126: init_per_testcase(xmpp_stanzas_counts_are_reported = CN, Config) ->
  127:     create_events_collection(),
  128:     enable_system_metrics(mim()),
  129:     Config1 = escalus:create_users(Config, escalus:get_users([alice, bob])),
  130:     escalus:init_per_testcase(CN, Config1);
  131: init_per_testcase(rdbms_module_opts_are_reported = CN, Config) ->
  132:     case mongoose_helper:is_rdbms_enabled(host_type()) of
  133:         false ->
  134:             {skip, "RDBMS is not available"};
  135:         true ->
  136:             create_events_collection(),
  137:             dynamic_modules:ensure_modules(host_type(), required_modules(CN)),
  138:             enable_system_metrics(mim()),
  139:             Config
  140:     end;
  141: init_per_testcase(module_opts_are_reported = CN, Config) ->
  142:     create_events_collection(),
  143:     dynamic_modules:ensure_modules(host_type(), required_modules(CN)),
  144:     enable_system_metrics(mim()),
  145:     Config;
  146: init_per_testcase(_TestcaseName, Config) ->
  147:     create_events_collection(),
  148:     enable_system_metrics(mim()),
  149:     Config.
  150: 
  151: end_per_testcase(system_metrics_are_not_reported_when_not_allowed, Config) ->
  152:     clear_events_collection(),
  153:     delete_prev_client_id(mim()),
  154:     Config;
  155: end_per_testcase(all_clustered_mongooses_report_the_same_client_id , Config) ->
  156:     clear_events_collection(),
  157:     delete_prev_client_id(mim()),
  158:     Nodes = [mim(), mim2()],
  159:     [ begin delete_prev_client_id(Node), disable_system_metrics(Node) end || Node <- Nodes ],
  160:     distributed_helper:remove_node_from_cluster(mim2(), Config),
  161:     Config;
  162: end_per_testcase(xmpp_stanzas_counts_are_reported = CN, Config) ->
  163:     clear_events_collection(),
  164:     disable_system_metrics(mim()),
  165:     escalus:delete_users(Config, escalus:get_users([alice, bob])),
  166:     escalus:end_per_testcase(CN, Config);
  167: end_per_testcase(_TestcaseName, Config) ->
  168:     clear_events_collection(),
  169:     disable_system_metrics(mim()),
  170:     delete_prev_client_id(mim()),
  171:     Config.
  172: 
  173: 
  174: %%--------------------------------------------------------------------
  175: %% Tests
  176: %%--------------------------------------------------------------------
  177: system_metrics_are_not_reported_when_not_allowed(_Config) ->
  178:     true = system_metrics_service_is_disabled(mim()).
  179: 
  180: periodic_report_available(_Config) ->
  181:     ReportsNumber = get_events_collection_size(),
  182:     mongoose_helper:wait_until(
  183:         fun() ->
  184:                 NewReportsNumber = get_events_collection_size(),
  185:                 NewReportsNumber > ReportsNumber + 1
  186:         end,
  187:         true).
  188: 
  189: all_clustered_mongooses_report_the_same_client_id(_Config) ->
  190:     mongoose_helper:wait_until(fun is_host_count_reported/0, true),
  191:     all_event_have_the_same_client_id().
  192: 
  193: system_metrics_are_reported_to_google_analytics_when_mim_starts(_Config) ->
  194:     mongoose_helper:wait_until(fun is_host_count_reported/0, true),
  195:     mongoose_helper:wait_until(fun are_modules_reported/0, true),
  196:     mongoose_helper:wait_until(fun events_are_reported_to_primary_tracking_id/0, true),
  197:     all_event_have_the_same_client_id().
  198: 
  199: system_metrics_are_reported_to_configurable_google_analytics(_Config) ->
  200:     mongoose_helper:wait_until(fun is_host_count_reported/0, true),
  201:     mongoose_helper:wait_until(fun are_modules_reported/0, true),
  202:     mongoose_helper:wait_until(fun events_are_reported_to_both_tracking_ids/0, true),
  203:     all_event_have_the_same_client_id().
  204: 
  205: system_metrics_are_reported_to_a_json_file(_Config) ->
  206:     ReportFilePath = rpc(mim(), mongoose_system_metrics_file, location, []),
  207:     ReportLastModified = rpc(mim(), filelib, last_modified, [ReportFilePath]),
  208:     Fun = fun() ->
  209:         ReportLastModified < rpc(mim(), filelib, last_modified, [ReportFilePath])
  210:     end,
  211:     mongoose_helper:wait_until(Fun, true),
  212:     %% now we read the content of the file and check if it's a valid JSON
  213:     {ok, File} = rpc(mim(), file, read_file, [ReportFilePath]),
  214:     jiffy:decode(File).
  215: 
  216: module_opts_are_reported(_Config) ->
  217:     mongoose_helper:wait_until(fun are_modules_reported/0, true),
  218:     Backend = mongoose_helper:mnesia_or_rdbms_backend(),
  219:     MemBackend = ct_helper:get_internal_database(),
  220:     check_module_backend(mod_bosh, MemBackend),
  221:     check_module_backend(mod_event_pusher, push),
  222:     check_module_backend(mod_event_pusher_push, Backend),
  223:     check_module_backend(mod_http_upload, s3),
  224:     check_module_backend(mod_last, Backend),
  225:     check_module_backend(mod_muc, Backend),
  226:     check_module_opt(mod_muc, <<"online_backend">>, atom_to_binary(MemBackend)),
  227:     check_module_backend(mod_muc_light, Backend),
  228:     check_module_backend(mod_offline, Backend),
  229:     check_module_backend(mod_privacy, Backend),
  230:     check_module_backend(mod_private, Backend),
  231:     check_module_backend(mod_pubsub, Backend),
  232:     check_module_opt(mod_push_service_mongoosepush, <<"api_version">>, <<"v3">>),
  233:     check_module_backend(mod_roster, Backend),
  234:     check_module_backend(mod_vcard, Backend).
  235: 
  236: rdbms_module_opts_are_reported(_Config) ->
  237:     mongoose_helper:wait_until(fun are_modules_reported/0, true),
  238:     check_module_backend(mod_auth_token, rdbms),
  239:     check_module_backend(mod_inbox, rdbms),
  240:     check_module_backend(mod_mam, rdbms).
  241: 
  242: check_module_backend(Module, Backend) ->
  243:     check_module_opt(Module, <<"backend">>, atom_to_binary(Backend)).
  244: 
  245: mongoose_version_is_reported(_Config) ->
  246:     mongoose_helper:wait_until(fun is_mongoose_version_reported/0, true).
  247: 
  248: cluster_uptime_is_reported(_Config) ->
  249:     mongoose_helper:wait_until(fun is_cluster_uptime_reported/0, true).
  250: 
  251: xmpp_components_are_reported(Config) ->
  252:     CompOpts = ?config(component1, Config),
  253:     {Component, Addr, _} = connect_component(CompOpts),
  254:     mongoose_helper:wait_until(fun are_xmpp_components_reported/0, true),
  255:     mongoose_helper:wait_until(fun more_than_one_component_is_reported/0, true),
  256:     disconnect_component(Component, Addr).
  257: 
  258: api_are_reported(_Config) ->
  259:     mongoose_helper:wait_until(fun is_api_reported/0, true).
  260: 
  261: transport_mechanisms_are_reported(_Config) ->
  262:     mongoose_helper:wait_until(fun are_transport_mechanisms_reported/0, true).
  263: 
  264: outgoing_pools_are_reported(_Config) ->
  265:     mongoose_helper:wait_until(fun are_outgoing_pools_reported/0, true).
  266: 
  267: xmpp_stanzas_counts_are_reported(Config) ->
  268:     escalus:story(Config, [{alice,1}, {bob,1}], fun(Alice, Bob) ->
  269:         mongoose_helper:wait_until(fun is_message_count_reported/0, true),
  270:         mongoose_helper:wait_until(fun is_iq_count_reported/0, true),
  271:         Sent = get_metric_value(<<"xmppMessageSent">>),
  272:         Received = get_metric_value(<<"xmppMessageReceived">>),
  273:         escalus:send(Alice, escalus_stanza:chat_to(Bob, <<"Hi">>)),
  274:         escalus:assert(is_chat_message, [<<"Hi">>], escalus:wait_for_stanza(Bob)),
  275:         F = fun() -> assert_message_count_is_incremented(Sent, Received) end,
  276:         mongoose_helper:wait_until(F, ok, #{sleep_time => 500, time_left => timer:seconds(20)})
  277:     end).
  278: 
  279: config_type_is_reported(_Config) ->
  280:     mongoose_helper:wait_until(fun is_config_type_reported/0, true).
  281: 
  282: just_removed_from_config_logs_question(_Config) ->
  283:     disable_system_metrics(mim3()),
  284:     %% WHEN
  285:     Result = distributed_helper:rpc(
  286:                mim3(), service_mongoose_system_metrics, verify_if_configured, []),
  287:     %% THEN
  288:     ?assertEqual(ignore, Result).
  289: 
  290: in_config_unmodified_logs_request_for_agreement(_Config) ->
  291:     %% WHEN
  292:     disable_system_metrics(mim()),
  293:     logger_ct_backend:capture(warning),
  294:     enable_system_metrics(mim()),
  295:     %% THEN
  296:     FilterFun = fun(_, Msg) ->
  297:                         re:run(Msg, "MongooseIM docs", [global]) /= nomatch
  298:                 end,
  299:     mongoose_helper:wait_until(fun() -> length(logger_ct_backend:recv(FilterFun)) end, 1),
  300:     %% CLEAN
  301:     logger_ct_backend:stop_capture(),
  302:     disable_system_metrics(mim()).
  303: 
  304: in_config_with_explicit_no_report_goes_off_silently(_Config) ->
  305:     %% WHEN
  306:     logger_ct_backend:capture(warning),
  307:     start_system_metrics_service(mim(), #{report => false}),
  308:     logger_ct_backend:stop_capture(),
  309:     %% THEN
  310:     FilterFun = fun(warning, Msg) ->
  311:                         re:run(Msg, "MongooseIM docs", [global]) /= nomatch;
  312:                    (_,_) -> false
  313:                 end,
  314:     [] = logger_ct_backend:recv(FilterFun),
  315:     %% CLEAN
  316:     disable_system_metrics(mim()).
  317: 
  318: in_config_with_explicit_reporting_goes_on_silently(_Config) ->
  319:     %% WHEN
  320:     logger_ct_backend:capture(warning),
  321:     start_system_metrics_service(mim(), #{report => true}),
  322:     logger_ct_backend:stop_capture(),
  323:     %% THEN
  324:     FilterFun = fun(warning, Msg) ->
  325:                         re:run(Msg, "MongooseIM docs", [global]) /= nomatch;
  326:                    (_,_) -> false
  327:                 end,
  328:     [] = logger_ct_backend:recv(FilterFun),
  329:     %% CLEAN
  330:     disable_system_metrics(mim()).
  331: 
  332: %%--------------------------------------------------------------------
  333: %% Helpers
  334: %%--------------------------------------------------------------------
  335: 
  336: required_modules(CaseName) ->
  337:     lists:filter(fun({Module, _Opts}) -> is_module_supported(Module) end,
  338:                  modules_to_test(CaseName)).
  339: 
  340: modules_to_test(module_opts_are_reported) ->
  341:     Backend = mongoose_helper:mnesia_or_rdbms_backend(),
  342:     MemBackend = ct_helper:get_internal_database(),
  343:     [required_module(mod_bosh, #{backend => MemBackend}),
  344:      required_module(mod_event_pusher,
  345:                      #{push => config([modules, mod_event_pusher, push], #{backend => Backend})}),
  346:      required_module(mod_http_upload, s3),
  347:      required_module(mod_last, Backend),
  348:      required_module(mod_muc, #{backend => Backend, online_backend => MemBackend}),
  349:      required_module(mod_muc_light, Backend),
  350:      required_module(mod_offline, Backend),
  351:      required_module(mod_privacy, Backend),
  352:      required_module(mod_private, Backend),
  353:      required_module(mod_pubsub, Backend),
  354:      required_module(mod_push_service_mongoosepush),
  355:      required_module(mod_roster, Backend),
  356:      required_module(mod_vcard, Backend)];
  357: modules_to_test(rdbms_module_opts_are_reported) ->
  358:     [required_module(mod_auth_token),
  359:      required_module(mod_inbox),
  360:      required_module(mod_mam)].
  361: 
  362: required_module(Module) ->
  363:     required_module(Module, #{}).
  364: 
  365: required_module(Module, Backend) when is_atom(Backend) ->
  366:     {Module, mod_config(Module, #{backend => Backend})};
  367: required_module(Module, Opts) ->
  368:     {Module, mod_config(Module, Opts)}.
  369: 
  370: check_module_opt(Module, Key, Value) when is_binary(Key), is_binary(Value) ->
  371:     case is_module_supported(Module) of
  372:         true ->
  373:             ?assert(is_module_opt_reported(atom_to_binary(Module), Key, Value), {Module, Key, Value});
  374:         false ->
  375:             ct:log("Skipping unsupported module ~p", [Module])
  376:     end.
  377: 
  378: is_module_supported(Module) ->
  379:     is_host_type_static() orelse supports_dynamic_domains(Module).
  380: 
  381: is_host_type_static() ->
  382:     rpc(mim(), mongoose_domain_core, is_static, [host_type()]).
  383: 
  384: supports_dynamic_domains(Module) ->
  385:     rpc(mim(), gen_mod, does_module_support, [Module, dynamic_domains]).
  386: 
  387: all_event_have_the_same_client_id() ->
  388:     Tab = ets:tab2list(?ETS_TABLE),
  389:     UniqueSortedTab = lists:usort([Cid || #event{client_id = Cid} <- Tab]),
  390:     1 = length(UniqueSortedTab).
  391: 
  392: is_host_count_reported() ->
  393:     is_in_table(<<"host_count">>).
  394: 
  395: are_modules_reported() ->
  396:     is_in_table(<<"module">>).
  397: 
  398: is_in_table(EventName) ->
  399:     Tab = ets:tab2list(?ETS_TABLE),
  400:     lists:any(
  401:         fun(#event{name = Name, params = Params}) ->
  402:             verify_name(Name, EventName, Params)
  403:         end, Tab).
  404: 
  405: verify_name(<<"module_with_opt">>, <<"module">>, Params) ->
  406:     Module = maps:get(<<"module">>, Params),
  407:     Result = re:run(Module, "^mod_.*"),
  408:     case Result of
  409:         {match, _Captured} -> true;
  410:         nomatch -> false
  411:     end;
  412: verify_name(Name, Name, _) ->
  413:     true;
  414: verify_name(_, _, _) ->
  415:     false.
  416: 
  417: get_events_collection_size() ->
  418:     ets:info(?ETS_TABLE, size).
  419: 
  420: enable_system_metrics(Node) ->
  421:     enable_system_metrics(Node, #{initial_report => 100, periodic_report => 100}).
  422: 
  423: enable_system_metrics_with_configurable_tracking_id(Node) ->
  424:     enable_system_metrics(Node, #{initial_report => 100, periodic_report => 100,
  425:                                   tracking_id => ?TRACKING_ID_EXTRA}).
  426: 
  427: enable_system_metrics(Node, Opts) ->
  428:     UrlArgs = [google_analytics_url, ?SERVER_URL],
  429:     ok = mongoose_helper:successful_rpc(Node, mongoose_config, set_opt, UrlArgs),
  430:     start_system_metrics_service(Node, Opts).
  431: 
  432: start_system_metrics_service(Node, ExtraOpts) ->
  433:     Opts = config([services, service_mongoose_system_metrics], ExtraOpts),
  434:     dynamic_services:ensure_started(Node, service_mongoose_system_metrics, Opts).
  435: 
  436: disable_system_metrics(Node) ->
  437:     dynamic_services:ensure_stopped(Node, service_mongoose_system_metrics),
  438:     mongoose_helper:successful_rpc(Node, mongoose_config, unset_opt, [ google_analytics_url ]).
  439: 
  440: delete_prev_client_id(Node) ->
  441:     mongoose_helper:successful_rpc(Node, mnesia, delete_table, [service_mongoose_system_metrics]).
  442: 
  443: create_events_collection() ->
  444:     ets:new(?ETS_TABLE, [duplicate_bag, named_table, public]).
  445: 
  446: clear_events_collection() ->
  447:     ets:delete_all_objects(?ETS_TABLE).
  448: 
  449: system_metrics_service_is_enabled(Node) ->
  450:     Pid = distributed_helper:rpc(Node, erlang, whereis, [service_mongoose_system_metrics]),
  451:     erlang:is_pid(Pid).
  452: 
  453: system_metrics_service_is_disabled(Node) ->
  454:     not system_metrics_service_is_enabled(Node).
  455: 
  456: events_are_reported_to_primary_tracking_id() ->
  457:     events_are_reported_to_tracking_ids([primary_tracking_id()]).
  458: 
  459: events_are_reported_to_both_tracking_ids() ->
  460:     events_are_reported_to_tracking_ids([primary_tracking_id(), ?TRACKING_ID_EXTRA]).
  461: 
  462: primary_tracking_id() ->
  463:     case os:getenv("CI") of
  464:         "true" -> ?TRACKING_ID_CI;
  465:         _ -> ?TRACKING_ID
  466:     end.
  467: 
  468: events_are_reported_to_tracking_ids(ConfiguredTrackingIds) ->
  469:     Tab = ets:tab2list(?ETS_TABLE),
  470:     ActualTrackingIds = lists:usort([InstanceId || #event{instance_id = InstanceId} <- Tab]),
  471:     ExpectedTrackingIds = lists:sort([list_to_binary(Tid) || #{id := Tid} <- ConfiguredTrackingIds]),
  472:     ExpectedTrackingIds =:= ActualTrackingIds.
  473: 
  474: is_feature_reported(EventName, Key) ->
  475:     length(match_events(EventName, Key)) > 0.
  476: 
  477: is_feature_reported(EventName, Key, Value) ->
  478:     length(match_events(EventName, Key, Value)) > 0.
  479: 
  480: is_module_opt_reported(Module, Key, Value) ->
  481:     length(get_matched_events_for_module(Module, Key, Value)) > 0.
  482: 
  483: is_mongoose_version_reported() ->
  484:     is_feature_reported(<<"cluster">>, <<"version">>).
  485: 
  486: is_cluster_uptime_reported() ->
  487:     is_feature_reported(<<"cluster">>, <<"uptime">>).
  488: 
  489: are_xmpp_components_reported() ->
  490:     is_feature_reported(<<"cluster">>, <<"component">>).
  491: 
  492: is_config_type_reported() ->
  493:     IsToml = is_feature_reported(<<"cluster">>, <<"config_type">>, <<"toml">>),
  494:     IsCfg = is_feature_reported(<<"cluster">>, <<"config_type">>, <<"cfg">>),
  495:     IsToml orelse IsCfg.
  496: 
  497: is_api_reported() ->
  498:     is_in_table(<<"http_api">>).
  499: 
  500: are_transport_mechanisms_reported() ->
  501:     is_in_table(<<"transport_mechanism">>).
  502: 
  503: are_outgoing_pools_reported() ->
  504:     is_in_table(<<"outgoing_pool">>).
  505: 
  506: is_iq_count_reported() ->
  507:     is_feature_reported(<<"xmpp_stanza_count">>,
  508:                         <<"stanza_type">>,
  509:                         <<"xmppIqSent">>).
  510: 
  511: is_message_count_reported() ->
  512:     XmppMessageSent = is_feature_reported(<<"xmpp_stanza_count">>,
  513:                                           <<"stanza_type">>,
  514:                                           <<"xmppMessageSent">>),
  515:     XmppMessageReceived = is_feature_reported(<<"xmpp_stanza_count">>,
  516:                                                <<"stanza_type">>,
  517:                                                <<"xmppMessageReceived">>),
  518:     XmppMessageSent andalso XmppMessageReceived. 
  519: 
  520: assert_message_count_is_incremented(Sent, Received) ->
  521:     assert_increment(<<"xmppMessageSent">>, Sent),
  522:     assert_increment(<<"xmppMessageReceived">>, Received).
  523: 
  524: assert_increment(EventCategory, InitialValue) ->
  525:     Events = match_events(<<"xmpp_stanza_count">>, <<"stanza_type">>, EventCategory),
  526:     % expect exactly one event with an increment of 1
  527:     SeekedEvent = [Event || Event = #event{params = #{<<"total">> := Total, <<"increment">> := 1}}
  528:         <- Events, Total == InitialValue + 1],
  529:     ?assertMatch([_], SeekedEvent).
  530: 
  531: get_metric_value(EventCategory) ->
  532:     [#event{params = #{<<"total">> := Value}} | _] = match_events(<<"xmpp_stanza_count">>, <<"stanza_type">>, EventCategory),
  533:     Value.
  534: 
  535: more_than_one_component_is_reported() ->
  536:     Events = match_events(<<"cluster">>),
  537:     lists:any(fun(#event{params = Params}) ->
  538:                        maps:get(<<"component">>, Params) > 0
  539:               end, Events).
  540: 
  541: match_events(EventName) ->
  542:     ets:match_object(?ETS_TABLE, #event{name = EventName, _ = '_'}).
  543: 
  544: match_events(EventName, ParamKey) ->
  545:     Res = ets:match_object(?ETS_TABLE, #event{name = EventName, _ = '_'}),
  546:     [Event || Event = #event{params = #{ParamKey := _}} <- Res].
  547: 
  548: match_events(EventName, ParamKey, ParamValue) ->
  549:     Res = ets:match_object(?ETS_TABLE, #event{name = EventName, _ = '_'}),
  550:     [Event || Event = #event{params = #{ParamKey := Value}} <- Res, Value == ParamValue].
  551: 
  552: get_matched_events_for_module(ParamModule, Key, ParamValue) ->
  553:     Res = ets:match_object(?ETS_TABLE, #event{name = <<"module_with_opt">>, _ = '_'}),
  554:     [Event || Event = #event{params = #{<<"module">> := Module, Key := Value}} <- Res,
  555:          Value == ParamValue, Module == ParamModule].
  556: 
  557: %%--------------------------------------------------------------------
  558: %% Cowboy handlers
  559: %%--------------------------------------------------------------------
  560: handler_init(Req0) ->
  561:     {ok, Body, Req} = cowboy_req:read_body(Req0),
  562:     #{measurement_id := InstanceId, api_secret := AppSecret} = cowboy_req:match_qs([measurement_id, api_secret], Req0),
  563:     BodyMap = jiffy:decode(Body, [return_maps]),
  564:     EventTab = maps:get(<<"events">>, BodyMap), 
  565:     ClientID = maps:get(<<"client_id">>, BodyMap),
  566:     lists:map(
  567:         fun(Event) ->
  568:             EventRecord = #event{name = maps:get(<<"name">>, Event),
  569:                                  params = maps:get(<<"params">>, Event),
  570:                                  client_id = ClientID,
  571:                                  instance_id = InstanceId,
  572:                                  app_secret = AppSecret},
  573:             %% TODO there is a race condition when table is not available
  574:             ets:insert(?ETS_TABLE, EventRecord)
  575:         end, EventTab),
  576:     Req1 = cowboy_req:reply(200, #{}, <<>>, Req),
  577:     {ok, Req1, no_state}.