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: check_module_backend(mod_bosh, mnesia), 220: check_module_backend(mod_event_pusher, push), 221: check_module_backend(mod_event_pusher_push, Backend), 222: check_module_backend(mod_http_upload, s3), 223: check_module_backend(mod_last, Backend), 224: check_module_backend(mod_muc, Backend), 225: check_module_backend(mod_muc_light, Backend), 226: check_module_backend(mod_offline, Backend), 227: check_module_backend(mod_privacy, Backend), 228: check_module_backend(mod_private, Backend), 229: check_module_backend(mod_pubsub, Backend), 230: check_module_opt(mod_push_service_mongoosepush, <<"api_version">>, <<"v3">>), 231: check_module_backend(mod_roster, Backend), 232: check_module_backend(mod_vcard, Backend). 233: 234: rdbms_module_opts_are_reported(_Config) -> 235: mongoose_helper:wait_until(fun are_modules_reported/0, true), 236: check_module_backend(mod_auth_token, rdbms), 237: check_module_backend(mod_inbox, rdbms), 238: check_module_backend(mod_mam, rdbms). 239: 240: check_module_backend(Module, Backend) -> 241: check_module_opt(Module, <<"backend">>, atom_to_binary(Backend)). 242: 243: mongoose_version_is_reported(_Config) -> 244: mongoose_helper:wait_until(fun is_mongoose_version_reported/0, true). 245: 246: cluster_uptime_is_reported(_Config) -> 247: mongoose_helper:wait_until(fun is_cluster_uptime_reported/0, true). 248: 249: xmpp_components_are_reported(Config) -> 250: CompOpts = ?config(component1, Config), 251: {Component, Addr, _} = connect_component(CompOpts), 252: mongoose_helper:wait_until(fun are_xmpp_components_reported/0, true), 253: mongoose_helper:wait_until(fun more_than_one_component_is_reported/0, true), 254: disconnect_component(Component, Addr). 255: 256: api_are_reported(_Config) -> 257: mongoose_helper:wait_until(fun is_api_reported/0, true). 258: 259: transport_mechanisms_are_reported(_Config) -> 260: mongoose_helper:wait_until(fun are_transport_mechanisms_reported/0, true). 261: 262: outgoing_pools_are_reported(_Config) -> 263: mongoose_helper:wait_until(fun are_outgoing_pools_reported/0, true). 264: 265: xmpp_stanzas_counts_are_reported(Config) -> 266: escalus:story(Config, [{alice,1}, {bob,1}], fun(Alice, Bob) -> 267: mongoose_helper:wait_until(fun is_message_count_reported/0, true), 268: mongoose_helper:wait_until(fun is_iq_count_reported/0, true), 269: Sent = get_metric_value(<<"xmppMessageSent">>), 270: Received = get_metric_value(<<"xmppMessageReceived">>), 271: escalus:send(Alice, escalus_stanza:chat_to(Bob, <<"Hi">>)), 272: escalus:assert(is_chat_message, [<<"Hi">>], escalus:wait_for_stanza(Bob)), 273: F = fun() -> assert_message_count_is_incremented(Sent, Received) end, 274: mongoose_helper:wait_until(F, ok, #{sleep_time => 500, time_left => timer:seconds(20)}) 275: end). 276: 277: config_type_is_reported(_Config) -> 278: mongoose_helper:wait_until(fun is_config_type_reported/0, true). 279: 280: just_removed_from_config_logs_question(_Config) -> 281: disable_system_metrics(mim3()), 282: %% WHEN 283: Result = distributed_helper:rpc( 284: mim3(), service_mongoose_system_metrics, verify_if_configured, []), 285: %% THEN 286: ?assertEqual(ignore, Result). 287: 288: in_config_unmodified_logs_request_for_agreement(_Config) -> 289: %% WHEN 290: disable_system_metrics(mim()), 291: logger_ct_backend:capture(warning), 292: enable_system_metrics(mim()), 293: %% THEN 294: FilterFun = fun(_, Msg) -> 295: re:run(Msg, "MongooseIM docs", [global]) /= nomatch 296: end, 297: mongoose_helper:wait_until(fun() -> length(logger_ct_backend:recv(FilterFun)) end, 1), 298: %% CLEAN 299: logger_ct_backend:stop_capture(), 300: disable_system_metrics(mim()). 301: 302: in_config_with_explicit_no_report_goes_off_silently(_Config) -> 303: %% WHEN 304: logger_ct_backend:capture(warning), 305: start_system_metrics_service(mim(), #{report => false}), 306: logger_ct_backend:stop_capture(), 307: %% THEN 308: FilterFun = fun(warning, Msg) -> 309: re:run(Msg, "MongooseIM docs", [global]) /= nomatch; 310: (_,_) -> false 311: end, 312: [] = logger_ct_backend:recv(FilterFun), 313: %% CLEAN 314: disable_system_metrics(mim()). 315: 316: in_config_with_explicit_reporting_goes_on_silently(_Config) -> 317: %% WHEN 318: logger_ct_backend:capture(warning), 319: start_system_metrics_service(mim(), #{report => true}), 320: logger_ct_backend:stop_capture(), 321: %% THEN 322: FilterFun = fun(warning, Msg) -> 323: re:run(Msg, "MongooseIM docs", [global]) /= nomatch; 324: (_,_) -> false 325: end, 326: [] = logger_ct_backend:recv(FilterFun), 327: %% CLEAN 328: disable_system_metrics(mim()). 329: 330: %%-------------------------------------------------------------------- 331: %% Helpers 332: %%-------------------------------------------------------------------- 333: 334: required_modules(CaseName) -> 335: lists:filter(fun({Module, _Opts}) -> is_module_supported(Module) end, 336: modules_to_test(CaseName)). 337: 338: modules_to_test(module_opts_are_reported) -> 339: Backend = mongoose_helper:mnesia_or_rdbms_backend(), 340: [required_module(mod_bosh), 341: required_module(mod_event_pusher, 342: #{push => config([modules, mod_event_pusher, push], #{backend => Backend})}), 343: required_module(mod_http_upload, s3), 344: required_module(mod_last, Backend), 345: required_module(mod_muc, Backend), 346: required_module(mod_muc_light, Backend), 347: required_module(mod_offline, Backend), 348: required_module(mod_privacy, Backend), 349: required_module(mod_private, Backend), 350: required_module(mod_pubsub, Backend), 351: required_module(mod_push_service_mongoosepush), 352: required_module(mod_roster, Backend), 353: required_module(mod_vcard, Backend)]; 354: modules_to_test(rdbms_module_opts_are_reported) -> 355: [required_module(mod_auth_token), 356: required_module(mod_inbox), 357: required_module(mod_mam)]. 358: 359: required_module(Module) -> 360: required_module(Module, #{}). 361: 362: required_module(Module, Backend) when is_atom(Backend) -> 363: {Module, mod_config(Module, #{backend => Backend})}; 364: required_module(Module, Opts) -> 365: {Module, mod_config(Module, Opts)}. 366: 367: check_module_opt(Module, Key, Value) -> 368: case is_module_supported(Module) of 369: true -> 370: ?assertEqual(true, is_module_opt_reported(atom_to_binary(Module), Key, Value)); 371: false -> 372: ct:log("Skipping unsupported module ~p", [Module]) 373: end. 374: 375: is_module_supported(Module) -> 376: is_host_type_static() orelse supports_dynamic_domains(Module). 377: 378: is_host_type_static() -> 379: rpc(mim(), mongoose_domain_core, is_static, [host_type()]). 380: 381: supports_dynamic_domains(Module) -> 382: rpc(mim(), gen_mod, does_module_support, [Module, dynamic_domains]). 383: 384: all_event_have_the_same_client_id() -> 385: Tab = ets:tab2list(?ETS_TABLE), 386: UniqueSortedTab = lists:usort([Cid || #event{client_id = Cid} <- Tab]), 387: 1 = length(UniqueSortedTab). 388: 389: is_host_count_reported() -> 390: is_in_table(<<"host_count">>). 391: 392: are_modules_reported() -> 393: is_in_table(<<"module">>). 394: 395: is_in_table(EventName) -> 396: Tab = ets:tab2list(?ETS_TABLE), 397: lists:any( 398: fun(#event{name = Name, params = Params}) -> 399: verify_name(Name, EventName, Params) 400: end, Tab). 401: 402: verify_name(<<"module_with_opt">>, <<"module">>, Params) -> 403: Module = maps:get(<<"module">>, Params), 404: Result = re:run(Module, "^mod_.*"), 405: case Result of 406: {match, _Captured} -> true; 407: nomatch -> false 408: end; 409: verify_name(Name, Name, _) -> 410: true; 411: verify_name(_, _, _) -> 412: false. 413: 414: get_events_collection_size() -> 415: ets:info(?ETS_TABLE, size). 416: 417: enable_system_metrics(Node) -> 418: enable_system_metrics(Node, #{initial_report => 100, periodic_report => 100}). 419: 420: enable_system_metrics_with_configurable_tracking_id(Node) -> 421: enable_system_metrics(Node, #{initial_report => 100, periodic_report => 100, 422: tracking_id => ?TRACKING_ID_EXTRA}). 423: 424: enable_system_metrics(Node, Opts) -> 425: UrlArgs = [google_analytics_url, ?SERVER_URL], 426: ok = mongoose_helper:successful_rpc(Node, mongoose_config, set_opt, UrlArgs), 427: start_system_metrics_service(Node, Opts). 428: 429: start_system_metrics_service(Node, ExtraOpts) -> 430: Opts = config([services, service_mongoose_system_metrics], ExtraOpts), 431: dynamic_services:ensure_started(Node, service_mongoose_system_metrics, Opts). 432: 433: disable_system_metrics(Node) -> 434: dynamic_services:ensure_stopped(Node, service_mongoose_system_metrics), 435: mongoose_helper:successful_rpc(Node, mongoose_config, unset_opt, [ google_analytics_url ]). 436: 437: delete_prev_client_id(Node) -> 438: mongoose_helper:successful_rpc(Node, mnesia, delete_table, [service_mongoose_system_metrics]). 439: 440: create_events_collection() -> 441: ets:new(?ETS_TABLE, [duplicate_bag, named_table, public]). 442: 443: clear_events_collection() -> 444: ets:delete_all_objects(?ETS_TABLE). 445: 446: system_metrics_service_is_enabled(Node) -> 447: Pid = distributed_helper:rpc(Node, erlang, whereis, [service_mongoose_system_metrics]), 448: erlang:is_pid(Pid). 449: 450: system_metrics_service_is_disabled(Node) -> 451: not system_metrics_service_is_enabled(Node). 452: 453: events_are_reported_to_primary_tracking_id() -> 454: events_are_reported_to_tracking_ids([primary_tracking_id()]). 455: 456: events_are_reported_to_both_tracking_ids() -> 457: events_are_reported_to_tracking_ids([primary_tracking_id(), ?TRACKING_ID_EXTRA]). 458: 459: primary_tracking_id() -> 460: case os:getenv("CI") of 461: "true" -> ?TRACKING_ID_CI; 462: _ -> ?TRACKING_ID 463: end. 464: 465: events_are_reported_to_tracking_ids(ConfiguredTrackingIds) -> 466: Tab = ets:tab2list(?ETS_TABLE), 467: ActualTrackingIds = lists:usort([InstanceId || #event{instance_id = InstanceId} <- Tab]), 468: ExpectedTrackingIds = lists:sort([list_to_binary(Tid) || #{id := Tid} <- ConfiguredTrackingIds]), 469: ExpectedTrackingIds =:= ActualTrackingIds. 470: 471: is_feature_reported(EventName, Key) -> 472: length(match_events(EventName, Key)) > 0. 473: 474: is_feature_reported(EventName, Key, Value) -> 475: length(match_events(EventName, Key, Value)) > 0. 476: 477: is_module_opt_reported(Module, Key, Value) -> 478: length(get_matched_events_for_module(Module, Key, Value)) > 0. 479: 480: is_mongoose_version_reported() -> 481: is_feature_reported(<<"cluster">>, <<"version">>). 482: 483: is_cluster_uptime_reported() -> 484: is_feature_reported(<<"cluster">>, <<"uptime">>). 485: 486: are_xmpp_components_reported() -> 487: is_feature_reported(<<"cluster">>, <<"component">>). 488: 489: is_config_type_reported() -> 490: IsToml = is_feature_reported(<<"cluster">>, <<"config_type">>, <<"toml">>), 491: IsCfg = is_feature_reported(<<"cluster">>, <<"config_type">>, <<"cfg">>), 492: IsToml orelse IsCfg. 493: 494: is_api_reported() -> 495: is_in_table(<<"http_api">>). 496: 497: are_transport_mechanisms_reported() -> 498: is_in_table(<<"transport_mechanism">>). 499: 500: are_outgoing_pools_reported() -> 501: is_in_table(<<"outgoing_pool">>). 502: 503: is_iq_count_reported() -> 504: is_feature_reported(<<"xmpp_stanza_count">>, 505: <<"stanza_type">>, 506: <<"xmppIqSent">>). 507: 508: is_message_count_reported() -> 509: XmppMessageSent = is_feature_reported(<<"xmpp_stanza_count">>, 510: <<"stanza_type">>, 511: <<"xmppMessageSent">>), 512: XmppMessageReceived = is_feature_reported(<<"xmpp_stanza_count">>, 513: <<"stanza_type">>, 514: <<"xmppMessageReceived">>), 515: XmppMessageSent andalso XmppMessageReceived. 516: 517: assert_message_count_is_incremented(Sent, Received) -> 518: assert_increment(<<"xmppMessageSent">>, Sent), 519: assert_increment(<<"xmppMessageReceived">>, Received). 520: 521: assert_increment(EventCategory, InitialValue) -> 522: Events = match_events(<<"xmpp_stanza_count">>, <<"stanza_type">>, EventCategory), 523: % expect exactly one event with an increment of 1 524: SeekedEvent = [Event || Event = #event{params = #{<<"total">> := Total, <<"increment">> := 1}} 525: <- Events, Total == InitialValue + 1], 526: ?assertMatch([_], SeekedEvent). 527: 528: get_metric_value(EventCategory) -> 529: [#event{params = #{<<"total">> := Value}} | _] = match_events(<<"xmpp_stanza_count">>, <<"stanza_type">>, EventCategory), 530: Value. 531: 532: more_than_one_component_is_reported() -> 533: Events = match_events(<<"cluster">>), 534: lists:any(fun(#event{params = Params}) -> 535: maps:get(<<"component">>, Params) > 0 536: end, Events). 537: 538: match_events(EventName) -> 539: ets:match_object(?ETS_TABLE, #event{name = EventName, _ = '_'}). 540: 541: match_events(EventName, ParamKey) -> 542: Res = ets:match_object(?ETS_TABLE, #event{name = EventName, _ = '_'}), 543: [Event || Event = #event{params = #{ParamKey := _}} <- Res]. 544: 545: match_events(EventName, ParamKey, ParamValue) -> 546: Res = ets:match_object(?ETS_TABLE, #event{name = EventName, _ = '_'}), 547: [Event || Event = #event{params = #{ParamKey := Value}} <- Res, Value == ParamValue]. 548: 549: get_matched_events_for_module(ParamModule, Key, ParamValue) -> 550: Res = ets:match_object(?ETS_TABLE, #event{name = <<"module_with_opt">>, _ = '_'}), 551: [Event || Event = #event{params = #{<<"module">> := Module, Key := Value}} <- Res, 552: Value == ParamValue, Module == ParamModule]. 553: 554: %%-------------------------------------------------------------------- 555: %% Cowboy handlers 556: %%-------------------------------------------------------------------- 557: handler_init(Req0) -> 558: {ok, Body, Req} = cowboy_req:read_body(Req0), 559: #{measurement_id := InstanceId, api_secret := AppSecret} = cowboy_req:match_qs([measurement_id, api_secret], Req0), 560: BodyMap = jiffy:decode(Body, [return_maps]), 561: EventTab = maps:get(<<"events">>, BodyMap), 562: ClientID = maps:get(<<"client_id">>, BodyMap), 563: lists:map( 564: fun(Event) -> 565: EventRecord = #event{name = maps:get(<<"name">>, Event), 566: params = maps:get(<<"params">>, Event), 567: client_id = ClientID, 568: instance_id = InstanceId, 569: app_secret = AppSecret}, 570: %% TODO there is a race condition when table is not available 571: ets:insert(?ETS_TABLE, EventRecord) 572: end, EventTab), 573: Req1 = cowboy_req:reply(200, #{}, <<>>, Req), 574: {ok, Req1, no_state}.