diff --git a/.github/integ-config/detox-integ-all.yml b/.github/integ-config/detox-integ-all.yml index 8f52ec8a01e..f85636f1f5c 100644 --- a/.github/integ-config/detox-integ-all.yml +++ b/.github/integ-config/detox-integ-all.yml @@ -5,8 +5,8 @@ - test_name: 'integ_rn_ios_device_tracking' working_directory: amplify-js-samples-staging/samples/react-native/auth/deviceTracking timeout_minutes: 120 -# - test_name: 'integ_rn_ios_datastore_sqlite_adapter' -# working_directory: ~/amplify-js-samples-staging/samples/react-native/datastore/SQLiteAdapter +- test_name: 'integ_rn_ios_datastore_sqlite_adapter' + working_directory: amplify-js-samples-staging/samples/react-native/datastore/SQLiteAdapter # - test_name: 'integ_rn_android_storage' # working_directory: ~/amplify-js-samples-staging/samples/react-native/storage/StorageApp # - test_name: 'integ_rn_android_storage_multipart_progress' diff --git a/.github/integ-config/integ-all.yml b/.github/integ-config/integ-all.yml index a0623191edb..8dd045a6025 100644 --- a/.github/integ-config/integ-all.yml +++ b/.github/integ-config/integ-all.yml @@ -9,290 +9,304 @@ extended_browser_list: &extended_browser_list tests: # DATASTORE - # - test_name: integ_datastore_auth-owner-based-default - # desc: 'DataStore Auth' - # framework: react - # category: datastore - # sample_name: owner-based-default - # spec: owner-based-default - # browser: *minimal_browser_list - # - test_name: integ_datastore_auth-static-user-pool-groups-default - # desc: 'DataStore Auth' - # framework: react - # category: datastore - # sample_name: static-user-pool-groups-default - # spec: static-user-pool-groups-default - # browser: *minimal_browser_list - # - test_name: integ_datastore_auth-static-user-pool-groups-operations - # desc: 'DataStore Auth' - # framework: react - # category: datastore - # sample_name: static-user-pool-groups-operations - # spec: static-user-pool-groups-operations - # browser: *minimal_browser_list - # - test_name: integ_datastore_auth-owner-and-group-different-models-default - # desc: 'DataStore Auth' - # framework: react - # category: datastore - # sample_name: owner-and-group-different-models-default - # spec: owner-and-group-different-models-default - # browser: *minimal_browser_list - # - test_name: integ_datastore_auth-owner-and-group-same-model-default - # desc: 'DataStore Auth' - # framework: react - # category: datastore - # sample_name: owner-and-group-same-model-default - # spec: owner-and-group-same-model-default - # browser: *minimal_browser_list - # - test_name: integ_datastore_auth-owner-and-group-same-model-operations - # desc: 'DataStore Auth' - # framework: react - # category: datastore - # sample_name: owner-and-group-same-model-operations - # spec: owner-and-group-same-model-operations - # browser: *minimal_browser_list - # - test_name: integ_datastore_auth-dynamic-user-pool-groups-default - # desc: 'DataStore Auth' - # framework: react - # category: datastore - # sample_name: dynamic-user-pool-groups-default - # spec: dynamic-user-pool-groups-default - # browser: *minimal_browser_list - # - test_name: integ_datastore_auth-dynamic-user-pool-groups-owner-based-default - # desc: 'DataStore Auth' - # framework: react - # category: datastore - # sample_name: dynamic-user-pool-groups-owner-based-default - # spec: dynamic-user-pool-groups-owner-based-default - # browser: *minimal_browser_list - # - test_name: integ_datastore_auth-private-auth-default - # desc: 'DataStore Auth' - # framework: react - # category: datastore - # sample_name: private-auth-default - # spec: private-auth-default - # browser: *minimal_browser_list - # - test_name: integ_datastore_auth-private-auth-iam - # desc: 'DataStore Auth' - # framework: react - # category: datastore - # sample_name: private-auth-iam - # spec: private-auth-iam - # browser: *minimal_browser_list - # - test_name: integ_datastore_auth-public-auth-default - # desc: 'DataStore Auth' - # framework: react - # category: datastore - # sample_name: public-auth-default - # spec: public-auth-default - # browser: *minimal_browser_list - # - test_name: integ_datastore_auth-public-auth-iam - # desc: 'DataStore Auth' - # framework: react - # category: datastore - # sample_name: public-auth-iam - # spec: public-auth-iam - # browser: *minimal_browser_list - # - test_name: integ_datastore_auth-owner-custom-claim-default - # desc: 'DataStore Auth' - # framework: react - # category: datastore - # sample_name: owner-custom-claim-default - # spec: owner-custom-claim-default - # browser: *minimal_browser_list - # - test_name: integ_datastore_auth-owner-custom-field-default - # desc: 'DataStore Auth' - # framework: react - # category: datastore - # sample_name: owner-custom-field-default - # spec: owner-custom-field-default - # browser: *minimal_browser_list - # - test_name: integ_datastore_auth_v2-owner-based-default - # desc: 'DataStore Auth CLI v2' - # framework: react - # category: datastore - # sample_name: v2/owner-based-default-v2 - # spec: owner-based-default - # browser: *minimal_browser_list - # - test_name: integ_datastore_auth_v2-static-user-pool-groups-default - # desc: 'DataStore Auth CLI v2' - # framework: react - # category: datastore - # sample_name: v2/static-user-pool-groups-default-v2 - # spec: static-user-pool-groups-default - # browser: *minimal_browser_list - # - test_name: integ_datastore_auth_v2-static-user-pool-groups-operations - # desc: 'DataStore Auth CLI v2' - # framework: react - # category: datastore - # sample_name: v2/static-user-pool-groups-operations-v2 - # spec: static-user-pool-groups-operations - # browser: *minimal_browser_list - # - test_name: integ_datastore_auth_v2-owner-and-group-different-models-default - # desc: 'DataStore Auth CLI v2' - # framework: react - # category: datastore - # sample_name: v2/owner-and-group-different-models-default-v2 - # spec: owner-and-group-different-models-default - # browser: *minimal_browser_list - # timeout_minutes: 45 - # retry_count: 10 - # - test_name: integ_datastore_auth_v2-owner-and-group-same-model-default - # desc: 'DataStore Auth CLI v2' - # framework: react - # category: datastore - # sample_name: v2/owner-and-group-same-model-default-v2 - # spec: owner-and-group-same-model-default - # browser: *minimal_browser_list - # - test_name: integ_datastore_auth_v2-owner-and-group-same-model-operations - # desc: 'DataStore Auth CLI v2' - # framework: react - # category: datastore - # sample_name: v2/owner-and-group-same-model-operations-v2 - # spec: owner-and-group-same-model-operations - # browser: *minimal_browser_list - # - test_name: integ_datastore_auth_v2-dynamic-user-pool-groups-default - # desc: 'DataStore Auth CLI v2' - # framework: react - # category: datastore - # sample_name: v2/dynamic-user-pool-groups-default-v2 - # spec: dynamic-user-pool-groups-default - # browser: *minimal_browser_list - # timeout_minutes: 45 - # retry_count: 10 - # - test_name: integ_datastore_auth_v2-dynamic-user-pool-groups-owner-based-default - # desc: 'DataStore Auth CLI v2' - # framework: react - # category: datastore - # sample_name: v2/dynamic-user-pool-groups-owner-based-default-v2 - # spec: dynamic-user-pool-groups-owner-based-default - # browser: *minimal_browser_list - # - test_name: integ_datastore_auth_v2-private-auth-default - # desc: 'DataStore Auth CLI v2' - # framework: react - # category: datastore - # sample_name: v2/private-auth-default-v2 - # spec: private-auth-default - # browser: *minimal_browser_list - # - test_name: integ_datastore_auth_v2-private-auth-iam - # desc: 'DataStore Auth CLI v2' - # framework: react - # category: datastore - # sample_name: v2/private-auth-iam-v2 - # spec: private-auth-iam - # browser: *minimal_browser_list - # - test_name: integ_datastore_auth_v2-public-auth-default - # desc: 'DataStore Auth CLI v2' - # framework: react - # category: datastore - # sample_name: v2/public-auth-default-v2 - # spec: public-auth-default - # browser: *minimal_browser_list - # - test_name: integ_datastore_auth_v2-public-auth-iam - # desc: 'DataStore Auth CLI v2' - # framework: react - # category: datastore - # sample_name: v2/public-auth-iam-v2 - # spec: public-auth-iam - # browser: *minimal_browser_list - # - test_name: integ_datastore_auth_v2-owner-custom-claim-default - # desc: 'DataStore Auth CLI v2' - # framework: react - # category: datastore - # sample_name: v2/owner-custom-claim-default-v2 - # spec: owner-custom-claim-default - # browser: *minimal_browser_list - # - test_name: integ_datastore_auth_v2-owner-custom-field-default - # desc: 'DataStore Auth CLI v2' - # framework: react - # category: datastore - # sample_name: v2/owner-custom-field-default-v2 - # spec: owner-custom-field-default - # browser: *minimal_browser_list - # - test_name: integ_react_datastore - # desc: 'React DataStore' - # framework: react - # category: datastore - # sample_name: [many-to-many] - # spec: many-to-many - # browser: *minimal_browser_list - # - test_name: integ_react_datastore_v2 - # desc: 'React DataStore CLI v2' - # framework: react - # category: datastore - # sample_name: [v2/many-to-many-v2] - # spec: many-to-many - # browser: *minimal_browser_list - # - test_name: integ_react_datastore_multi_auth_one_rule - # desc: 'React DataStore Multi-Auth - One Rule' - # framework: react - # category: datastore - # sample_name: [multi-auth] - # spec: multi-auth-one-rule - # browser: *minimal_browser_list - # - test_name: integ_react_datastore_multi_auth_one_rule_v2 - # desc: 'React DataStore Multi-Auth - One Rule CLI v2' - # framework: react - # category: datastore - # sample_name: [v2/multi-auth-v2] - # spec: multi-auth-one-rule - # browser: *minimal_browser_list - # - test_name: integ_react_datastore_multi_auth_two_rules - # desc: 'React DataStore Multi-Auth - Two Rules' - # framework: react - # category: datastore - # sample_name: [multi-auth] - # spec: multi-auth-two-rules - # browser: *minimal_browser_list - # - test_name: integ_react_datastore_multi_auth_two_rules_v2 - # desc: 'React DataStore Multi-Auth - Two Rules CLI v2' - # framework: react - # category: datastore - # sample_name: [v2/multi-auth-v2] - # spec: multi-auth-two-rules - # browser: *minimal_browser_list - # - test_name: integ_react_datastore_multi_auth_three_plus_rules - # desc: 'React DataStore Multi-Auth - Three Plus Rules' - # framework: react - # category: datastore - # sample_name: [multi-auth] - # spec: multi-auth-three-plus-rules - # browser: *minimal_browser_list - # - test_name: integ_react_datastore_multi_auth_three_plus_rules_v2 - # desc: 'React DataStore Multi-Auth - Three Plus Rules CLI v2' - # framework: react - # category: datastore - # sample_name: [v2/multi-auth-v2] - # spec: multi-auth-three-plus-rules - # browser: *minimal_browser_list - # - test_name: integ_react_datastore_subs_disabled - # desc: 'DataStore - Subs Disabled' - # framework: react - # category: datastore - # sample_name: [subs-disabled] - # spec: subs-disabled - # browser: *minimal_browser_list - # - test_name: integ_react_datastore_subs_disabled_v2 - # desc: 'DataStore - Subs Disabled CLI v2' - # framework: react - # category: datastore - # sample_name: [v2/subs-disabled-v2] - # spec: subs-disabled - # browser: *minimal_browser_list - # - test_name: integ_react_datastore_consecutive_saves - # desc: 'DataStore - Subs Disabled' - # framework: react - # category: datastore - # sample_name: [consecutive-saves] - # spec: consecutive-saves - # browser: *minimal_browser_list - # - test_name: integ_react_datastore_consecutive_saves_v2 - # desc: 'DataStore - Subs Disabled CLI v2' - # framework: react - # category: datastore - # sample_name: [v2/consecutive-saves-v2] - # spec: consecutive-saves - # browser: *minimal_browser_list + - test_name: integ_datastore_auth-owner-based-default + desc: 'DataStore Auth' + framework: react + category: datastore + sample_name: owner-based-default + spec: owner-based-default + browser: *minimal_browser_list + - test_name: integ_datastore_auth-static-user-pool-groups-default + desc: 'DataStore Auth' + framework: react + category: datastore + sample_name: static-user-pool-groups-default + spec: static-user-pool-groups-default + browser: *minimal_browser_list + - test_name: integ_datastore_auth-static-user-pool-groups-operations + desc: 'DataStore Auth' + framework: react + category: datastore + sample_name: static-user-pool-groups-operations + spec: static-user-pool-groups-operations + browser: *minimal_browser_list + - test_name: integ_datastore_auth-owner-and-group-different-models-default + desc: 'DataStore Auth' + framework: react + category: datastore + sample_name: owner-and-group-different-models-default + spec: owner-and-group-different-models-default + browser: *minimal_browser_list + - test_name: integ_datastore_auth-owner-and-group-same-model-default + desc: 'DataStore Auth' + framework: react + category: datastore + sample_name: owner-and-group-same-model-default + spec: owner-and-group-same-model-default + browser: *minimal_browser_list + - test_name: integ_datastore_auth-owner-and-group-same-model-operations + desc: 'DataStore Auth' + framework: react + category: datastore + sample_name: owner-and-group-same-model-operations + spec: owner-and-group-same-model-operations + browser: *minimal_browser_list + - test_name: integ_datastore_auth-dynamic-user-pool-groups-default + desc: 'DataStore Auth' + framework: react + category: datastore + sample_name: dynamic-user-pool-groups-default + spec: dynamic-user-pool-groups-default + browser: *minimal_browser_list + - test_name: integ_datastore_auth-dynamic-user-pool-groups-owner-based-default + desc: 'DataStore Auth' + framework: react + category: datastore + sample_name: dynamic-user-pool-groups-owner-based-default + spec: dynamic-user-pool-groups-owner-based-default + browser: *minimal_browser_list + - test_name: integ_datastore_auth-private-auth-default + desc: 'DataStore Auth' + framework: react + category: datastore + sample_name: private-auth-default + spec: private-auth-default + browser: *minimal_browser_list + - test_name: integ_datastore_auth-private-auth-iam + desc: 'DataStore Auth' + framework: react + category: datastore + sample_name: private-auth-iam + spec: private-auth-iam + browser: *minimal_browser_list + - test_name: integ_datastore_auth-public-auth-default + desc: 'DataStore Auth' + framework: react + category: datastore + sample_name: public-auth-default + spec: public-auth-default + browser: *minimal_browser_list + - test_name: integ_datastore_auth-public-auth-iam + desc: 'DataStore Auth' + framework: react + category: datastore + sample_name: public-auth-iam + spec: public-auth-iam + browser: *minimal_browser_list + - test_name: integ_datastore_auth-owner-custom-claim-default + desc: 'DataStore Auth' + framework: react + category: datastore + sample_name: owner-custom-claim-default + spec: owner-custom-claim-default + browser: *minimal_browser_list + - test_name: integ_datastore_auth-owner-custom-field-default + desc: 'DataStore Auth' + framework: react + category: datastore + sample_name: owner-custom-field-default + spec: owner-custom-field-default + browser: *minimal_browser_list + - test_name: integ_datastore_auth_v2-owner-based-default + desc: 'DataStore Auth CLI v2' + framework: react + category: datastore + sample_name: v2/owner-based-default-v2 + spec: owner-based-default + browser: *minimal_browser_list + - test_name: integ_datastore_auth_v2-static-user-pool-groups-default + desc: 'DataStore Auth CLI v2' + framework: react + category: datastore + sample_name: v2/static-user-pool-groups-default-v2 + spec: static-user-pool-groups-default + browser: *minimal_browser_list + - test_name: integ_datastore_auth_v2-static-user-pool-groups-operations + desc: 'DataStore Auth CLI v2' + framework: react + category: datastore + sample_name: v2/static-user-pool-groups-operations-v2 + spec: static-user-pool-groups-operations + browser: *minimal_browser_list + - test_name: integ_datastore_auth_v2-owner-and-group-different-models-default + desc: 'DataStore Auth CLI v2' + framework: react + category: datastore + sample_name: v2/owner-and-group-different-models-default-v2 + spec: owner-and-group-different-models-default + browser: *minimal_browser_list + timeout_minutes: 45 + retry_count: 10 + - test_name: integ_datastore_auth_v2-owner-and-group-same-model-default + desc: 'DataStore Auth CLI v2' + framework: react + category: datastore + sample_name: v2/owner-and-group-same-model-default-v2 + spec: owner-and-group-same-model-default + browser: *minimal_browser_list + - test_name: integ_datastore_auth_v2-owner-and-group-same-model-operations + desc: 'DataStore Auth CLI v2' + framework: react + category: datastore + sample_name: v2/owner-and-group-same-model-operations-v2 + spec: owner-and-group-same-model-operations + browser: *minimal_browser_list + - test_name: integ_datastore_auth_v2-dynamic-user-pool-groups-default + desc: 'DataStore Auth CLI v2' + framework: react + category: datastore + sample_name: v2/dynamic-user-pool-groups-default-v2 + spec: dynamic-user-pool-groups-default + browser: *minimal_browser_list + timeout_minutes: 45 + retry_count: 10 + - test_name: integ_datastore_auth_v2-dynamic-user-pool-groups-owner-based-default + desc: 'DataStore Auth CLI v2' + framework: react + category: datastore + sample_name: v2/dynamic-user-pool-groups-owner-based-default-v2 + spec: dynamic-user-pool-groups-owner-based-default + browser: *minimal_browser_list + - test_name: integ_datastore_auth_v2-private-auth-default + desc: 'DataStore Auth CLI v2' + framework: react + category: datastore + sample_name: v2/private-auth-default-v2 + spec: private-auth-default + browser: *minimal_browser_list + - test_name: integ_datastore_auth_v2-private-auth-iam + desc: 'DataStore Auth CLI v2' + framework: react + category: datastore + sample_name: v2/private-auth-iam-v2 + spec: private-auth-iam + browser: *minimal_browser_list + - test_name: integ_datastore_auth_v2-public-auth-default + desc: 'DataStore Auth CLI v2' + framework: react + category: datastore + sample_name: v2/public-auth-default-v2 + spec: public-auth-default + browser: *minimal_browser_list + - test_name: integ_datastore_auth_v2-public-auth-iam + desc: 'DataStore Auth CLI v2' + framework: react + category: datastore + sample_name: v2/public-auth-iam-v2 + spec: public-auth-iam + browser: *minimal_browser_list + - test_name: integ_datastore_auth_v2-owner-custom-claim-default + desc: 'DataStore Auth CLI v2' + framework: react + category: datastore + sample_name: v2/owner-custom-claim-default-v2 + spec: owner-custom-claim-default + browser: *minimal_browser_list + - test_name: integ_datastore_auth_v2-owner-custom-field-default + desc: 'DataStore Auth CLI v2' + framework: react + category: datastore + sample_name: v2/owner-custom-field-default-v2 + spec: owner-custom-field-default + browser: *minimal_browser_list + - test_name: integ_react_datastore + desc: 'React DataStore' + framework: react + category: datastore + sample_name: [many-to-many] + spec: many-to-many + browser: *minimal_browser_list + - test_name: integ_react_datastore_v2 + desc: 'React DataStore CLI v2' + framework: react + category: datastore + sample_name: [v2/many-to-many-v2] + spec: many-to-many + browser: *minimal_browser_list + - test_name: integ_react_datastore_multi_auth_one_rule + desc: 'React DataStore Multi-Auth - One Rule' + framework: react + category: datastore + sample_name: [multi-auth] + spec: multi-auth-one-rule + browser: *minimal_browser_list + - test_name: integ_react_datastore_multi_auth_one_rule_v2 + desc: 'React DataStore Multi-Auth - One Rule CLI v2' + framework: react + category: datastore + sample_name: [v2/multi-auth-v2] + spec: multi-auth-one-rule + browser: *minimal_browser_list + - test_name: integ_react_datastore_multi_auth_one_rule_oidc_v2 + desc: 'React DataStore Multi-Auth OIDC - One Rule CLI v2' + framework: react + category: datastore + sample_name: [v2/multi-auth-oidc-v2] + spec: multi-auth-one-rule-oidc + browser: *minimal_browser_list + - test_name: integ_react_datastore_multi_auth_two_rules + desc: 'React DataStore Multi-Auth - Two Rules' + framework: react + category: datastore + sample_name: [multi-auth] + spec: multi-auth-two-rules + browser: *minimal_browser_list + - test_name: integ_react_datastore_multi_auth_two_rules_v2 + desc: 'React DataStore Multi-Auth - Two Rules CLI v2' + framework: react + category: datastore + sample_name: [v2/multi-auth-v2] + spec: multi-auth-two-rules + browser: *minimal_browser_list + - test_name: integ_react_datastore_multi_auth_two_rules_oidc_v2 + desc: 'React DataStore Multi-Auth OIDC - Two Rules CLI v2' + framework: react + category: datastore + sample_name: [v2/multi-auth-oidc-v2] + spec: multi-auth-two-rules-oidc + browser: *minimal_browser_list + - test_name: integ_react_datastore_multi_auth_three_plus_rules + desc: 'React DataStore Multi-Auth - Three Plus Rules' + framework: react + category: datastore + sample_name: [multi-auth] + spec: multi-auth-three-plus-rules + browser: *minimal_browser_list + - test_name: integ_react_datastore_multi_auth_three_plus_rules_v2 + desc: 'React DataStore Multi-Auth - Three Plus Rules CLI v2' + framework: react + category: datastore + sample_name: [v2/multi-auth-v2] + spec: multi-auth-three-plus-rules + browser: *minimal_browser_list + - test_name: integ_react_datastore_subs_disabled + desc: 'DataStore - Subs Disabled' + framework: react + category: datastore + sample_name: [subs-disabled] + spec: subs-disabled + browser: *minimal_browser_list + - test_name: integ_react_datastore_subs_disabled_v2 + desc: 'DataStore - Subs Disabled CLI v2' + framework: react + category: datastore + sample_name: [v2/subs-disabled-v2] + spec: subs-disabled + browser: *minimal_browser_list + - test_name: integ_react_datastore_consecutive_saves + desc: 'DataStore - Subs Disabled' + framework: react + category: datastore + sample_name: [consecutive-saves] + spec: consecutive-saves + browser: *minimal_browser_list + - test_name: integ_react_datastore_consecutive_saves_v2 + desc: 'DataStore - Subs Disabled CLI v2' + framework: react + category: datastore + sample_name: [v2/consecutive-saves-v2] + spec: consecutive-saves + browser: *minimal_browser_list # - test_name: integ_react_datastore_observe_query # desc: 'DataStore - Observe Query' # framework: react @@ -307,20 +321,20 @@ tests: # sample_name: [v2/observe-query-v2] # spec: observe-query # browser: *minimal_browser_list - # - test_name: integ_react_datastore_schema_drift - # desc: 'DataStore - Schema Drift' - # framework: react - # category: datastore - # sample_name: [schema-drift] - # spec: schema-drift - # browser: *minimal_browser_list - # - test_name: integ_react_datastore_background_process_manager - # desc: 'DataStore - Background Process Manager' - # framework: react - # category: datastore - # sample_name: [v2/background-process-manager] - # spec: background-process-manager - # browser: *extended_browser_list + - test_name: integ_react_datastore_schema_drift + desc: 'DataStore - Schema Drift' + framework: react + category: datastore + sample_name: [schema-drift] + spec: schema-drift + browser: *minimal_browser_list + - test_name: integ_react_datastore_background_process_manager + desc: 'DataStore - Background Process Manager' + framework: react + category: datastore + sample_name: [v2/background-process-manager] + spec: background-process-manager + browser: *extended_browser_list # - test_name: integ_react_datastore_background_process_manager_webkit # desc: 'DataStore - Background Process Manager' # framework: react @@ -328,105 +342,112 @@ tests: # sample_name: [v2/background-process-manager] # spec: background-process-manager # browser: [webkit] - # - test_name: integ_react_datastore_cpk_related_models - # desc: 'DataStore - Custom Primary Key + Related Models' - # framework: react - # category: datastore - # sample_name: [v2/related-models] - # spec: cpk-related-models - # browser: *extended_browser_list - # timeout_minutes: 45 - # retry_count: 10 - # - test_name: integ_react_datastore_selective_sync - # desc: 'DataStore - Selective Sync' - # framework: react - # category: datastore - # sample_name: [selective-sync-v5] - # spec: selective-sync-v5 - # browser: *minimal_browser_list - # - test_name: integ_react_datastore_nested_predicate - # desc: 'DataStore - Nested Predicate' - # framework: react - # category: datastore - # sample_name: [nested-predicate] - # spec: nested-predicate - # browser: *minimal_browser_list - # - test_name: integ_react_datastore_docs_examples - # desc: 'DataStore - Docs Examples' - # framework: react - # category: datastore - # sample_name: [v2/amplify-docs-examples] - # spec: amplify-docs-examples - # browser: *minimal_browser_list - # timeout_minutes: 45 - # retry_count: 10 - # - test_name: integ_react_datastore_websocket_disruption - # desc: 'DataStore - WebSocket Disruption' - # framework: react - # category: datastore - # sample_name: [websocket-disruption] - # spec: websocket-disruption - # browser: *minimal_browser_list - # - test_name: integ_vanilla_js_datastore_basic_crud - # desc: 'Vanilla JS + Webpack 4 + DataStore - Basic CRUD' - # framework: javascript - # category: datastore - # sample_name: [basic-crud] - # browser: *minimal_browser_list - # spec: vanilla-js-basic-crud - # amplifyjs_dir: true - # timeout_minutes: 45 - # retry_count: 10 - # - test_name: integ_next_datastore_owner_auth - # desc: 'next owner auth' - # framework: next - # category: datastore - # sample_name: [owner-based-default] - # spec: next-owner-based-default - # browser: *minimal_browser_list - # - test_name: integ_next_datastore_13_basic - # desc: 'DataStore - Nextjs 13 build with SWC - basic JS app' - # framework: next - # category: datastore - # sample_name: [next-13-basic] - # spec: nextjs-13-basic - # browser: *minimal_browser_list - # - test_name: integ_next_datastore_13_js - # desc: 'DataStore - Nextjs 13 build with SWC - JS app' - # framework: next - # category: datastore - # sample_name: [next-13-js] - # spec: nextjs-13 - # browser: *minimal_browser_list - # - test_name: integ_vite_datastore_basic_crud - # desc: 'Vite + DataStore - Basic CRUD' - # framework: vite - # category: datastore - # sample_name: [v2/basic-crud] - # spec: vite-basic-crud - # # TODO: run on firefox - # browser: [chrome] - # timeout_minutes: 45 - # retry_count: 10 - # - test_name: integ_rollup_datastore_basic_crud - # desc: 'Rollup + DataStore - Basic CRUD' - # framework: rollup - # category: datastore - # sample_name: [rollup-basic-crud] - # spec: rollup-basic-crud - # # TODO: run on firefox - # browser: [chrome] - # timeout_minutes: 45 - # retry_count: 10 + - test_name: integ_react_datastore_cpk_related_models + desc: 'DataStore - Custom Primary Key + Related Models' + framework: react + category: datastore + sample_name: [v2/related-models] + spec: cpk-related-models + browser: *extended_browser_list + timeout_minutes: 45 + retry_count: 10 + - test_name: integ_react_datastore_selective_sync + desc: 'DataStore - Selective Sync' + framework: react + category: datastore + sample_name: [selective-sync-v5] + spec: selective-sync-v5 + browser: *minimal_browser_list + - test_name: integ_react_datastore_nested_predicate + desc: 'DataStore - Nested Predicate' + framework: react + category: datastore + sample_name: [nested-predicate] + spec: nested-predicate + browser: *minimal_browser_list + - test_name: integ_react_datastore_docs_examples + desc: 'DataStore - Docs Examples' + framework: react + category: datastore + sample_name: [v2/amplify-docs-examples] + spec: amplify-docs-examples + browser: *minimal_browser_list + timeout_minutes: 45 + retry_count: 10 + - test_name: integ_react_datastore_websocket_disruption + desc: 'DataStore - WebSocket Disruption' + framework: react + category: datastore + sample_name: [websocket-disruption] + spec: websocket-disruption + browser: *minimal_browser_list + - test_name: integ_vanilla_js_datastore_basic_crud + desc: 'Vanilla JS + Webpack 4 + DataStore - Basic CRUD' + framework: javascript + category: datastore + sample_name: [basic-crud] + browser: *minimal_browser_list + spec: vanilla-js-basic-crud + amplifyjs_dir: true + timeout_minutes: 45 + retry_count: 10 + - test_name: integ_next_datastore_owner_auth + desc: 'next owner auth' + framework: next + category: datastore + sample_name: [owner-based-default] + spec: next-owner-based-default + browser: *minimal_browser_list + - test_name: integ_next_datastore_13_basic + desc: 'DataStore - Nextjs 13 build with SWC - basic JS app' + framework: next + category: datastore + sample_name: [next-13-basic] + spec: nextjs-13-basic + browser: *minimal_browser_list + - test_name: integ_next_datastore_13_js + desc: 'DataStore - Nextjs 13 build with SWC - JS app' + framework: next + category: datastore + sample_name: [next-13-js] + spec: nextjs-13 + browser: *minimal_browser_list + - test_name: integ_vite_datastore_basic_crud + desc: 'Vite + DataStore - Basic CRUD' + framework: vite + category: datastore + sample_name: [v2/basic-crud] + spec: vite-basic-crud + # TODO: run on firefox + browser: [chrome] + timeout_minutes: 45 + retry_count: 10 + - test_name: integ_rollup_datastore_basic_crud + desc: 'Rollup + DataStore - Basic CRUD' + framework: rollup + category: datastore + sample_name: [rollup-basic-crud] + spec: rollup-basic-crud + # TODO: run on firefox + browser: [chrome] + timeout_minutes: 45 + retry_count: 10 # API - # - test_name: integ_react_graphql_api - # desc: React GraphQL API - # framework: react - # category: api - # sample_name: [graphql] - # spec: graphql - # browser: *minimal_browser_list + - test_name: integ_react_graphql_api + desc: React GraphQL API + framework: react + category: api + sample_name: [graphql] + spec: graphql + browser: *minimal_browser_list + - test_name: integ_react_api_optimistic_ui + desc: React GraphQL API Optimistic UI with Tan/ReactQuery + framework: react + category: api + sample_name: [optimistic-ui] + spec: optimistic-ui + browser: *minimal_browser_list # AUTH - test_name: integ_react_javascript_authentication @@ -436,14 +457,13 @@ tests: sample_name: [javascript-auth] spec: functional-auth browser: *minimal_browser_list - # TODO(v6) Migrate? - # - test_name: integ_react_auth_1_guest_to_authenticated_user - # desc: 'Guest to Authenticated User' - # framework: react - # category: auth - # sample_name: [guest-to-auth-user] - # spec: guest-to-auth-user - # browser: *minimal_browser_list + - test_name: integ_react_auth_1_guest_to_authenticated_user + desc: 'Guest to Authenticated User' + framework: react + category: auth + sample_name: [guest-to-auth-user] + spec: guest-to-auth-user + browser: *minimal_browser_list - test_name: integ_react_typescript_authentication desc: 'React Typescript Authentication' framework: react @@ -458,41 +478,47 @@ tests: sample_name: [credentials-auth] spec: credentials-auth browser: *minimal_browser_list - # - test_name: integ_react_auth_2_sign_in_after_sign_up - # desc: 'Sign In after Sign Up' - # framework: react - # category: auth - # sample_name: [auto-signin-after-signup] - # spec: auto-signin-after-signup - # browser: *minimal_browser_list - - test_name: integ_react_amazon_cognito_identity_js_cookie_storage - desc: 'amazon-cognito-identity-js-cookie-storage' + - test_name: integ_react_auth_2_sign_in_after_sign_up + desc: 'Sign In after Sign Up' framework: react category: auth - sample_name: [amazon-cognito-identity-js-cookie-storage] - spec: amazon-cognito-identity-js-cookie-storage + sample_name: [auto-signin-after-signup] + spec: auto-signin-after-signup browser: *minimal_browser_list - - test_name: integ_react_amazon_cognito_identity_js - desc: 'amazon-cognito-identity-js' + - test_name: integ_react_device_tracking + desc: 'cognito-device-tracking' framework: react category: auth - sample_name: [amazon-cognito-identity-js] - spec: amazon-cognito-identity-js + sample_name: [device-tracking] + spec: device-tracking browser: *minimal_browser_list - # - test_name: integ_react_device_tracking - # desc: 'cognito-device-tracking' - # framework: react - # category: auth - # sample_name: [device-tracking] - # spec: device-tracking - # browser: *minimal_browser_list - # - test_name: integ_react_delete_user - # desc: 'delete-user' - # framework: react - # category: auth - # sample_name: [delete-user] - # spec: delete-user - # browser: *minimal_browser_list + - test_name: integ_react_delete_user + desc: 'delete-user' + framework: react + category: auth + sample_name: [delete-user] + spec: delete-user + browser: *minimal_browser_list + - test_name: integ_next_auth_authenticator_and_ssr_page + desc: 'Authenticator and SSR page' + framework: next + category: auth + sample_name: [auth-ssr] + spec: auth-ssr + browser: *minimal_browser_list + - test_name: integ_next_auth_authenticator_and_rsc_page + desc: 'Authenticator and RSC page' + framework: next + category: auth + sample_name: [auth-rsc] + spec: auth-rsc + browser: [chrome] + timeout_minutes: 45 + retry_count: 10 + + # DISABLED Angular/Vue tests: + # TODO: delete tests or add custom ui logic to support them. + # - test_name: integ_angular_auth_angular_authenticator # desc: 'Angular Authenticator' # framework: angular @@ -507,14 +533,6 @@ tests: # sample_name: [amplify-authenticator] # spec: custom-authenticator # browser: *minimal_browser_list - # - test_name: integ_javascript_auth - # desc: 'JavaScript Auth CDN' - # framework: javascript - # category: auth - # sample_name: [auth-cdn] - # spec: auth-cdn - # browser: *minimal_browser_list - # amplifyjs_dir: true # - test_name: integ_vue_auth_legacy_vue_authenticator # desc: 'Legacy Vue Authenticator' # framework: vue @@ -529,31 +547,18 @@ tests: # sample_name: [authenticator-vue3] # spec: new-ui-authenticator # browser: *minimal_browser_list - # TODO(v6) Migrate once SSR updates available - # - test_name: integ_next_auth_authenticator_and_ssr_page - # desc: 'Authenticator and SSR page' - # framework: next - # category: auth - # sample_name: [auth-ssr] - # spec: auth-ssr - # browser: *minimal_browser_list - # TODO(v6) Migrate once SSR updates available - # - test_name: integ_next_auth_authenticator_and_rsc_page - # desc: 'Authenticator and RSC page' - # framework: next - # category: auth - # sample_name: [auth-rsc] - # spec: auth-rsc - # browser: [chrome] - # timeout_minutes: 45 - # retry_count: 10 - # - test_name: integ_next_auth_nextjs_auth_custom_implementation_with_ssr - # desc: 'NextJS Auth Custom Implementation with SSR' - # framework: next + + # DISABLED CDN test: + # TODO: delete this test after making sure CDN won't be supported + + # - test_name: integ_javascript_auth + # desc: 'JavaScript Auth CDN' + # framework: javascript # category: auth - # sample_name: [custom-auth-ssr] - # spec: authenticator + # sample_name: [auth-cdn] + # spec: auth-cdn # browser: *minimal_browser_list + # amplifyjs_dir: true # ANALYTICS - test_name: integ_react_analytics_pinpoint @@ -565,6 +570,24 @@ tests: # Temp fix: browser: *minimal_browser_list + - test_name: integ_react_analytics_personalize_auth + desc: 'Test record API for Personalize with authenticated user' + framework: react + category: analytics + sample_name: [personalize-test] + spec: personalize + # Temp fix: + browser: *minimal_browser_list + + - test_name: integ_react_analytics_personalize_unauth + desc: 'Test record API for Personalize with guest user' + framework: react + category: analytics + sample_name: [personalize-test] + spec: personalize-unauth + # Temp fix: + browser: *minimal_browser_list + - test_name: integ_react_analytics_kinesis_data_firehose_auth desc: 'Test record API for KDF with authenticated user' framework: react @@ -584,22 +607,22 @@ tests: browser: *minimal_browser_list # GEO - # - test_name: integ_react_geo_display_map - # desc: 'Display Map' - # framework: react - # category: geo - # sample_name: [display-map] - # spec: display-map - # # Temp fix: - # browser: [chrome] - # - test_name: integ_react_geo_search_outside_map - # desc: 'Search Outside Map' - # framework: react - # category: geo - # sample_name: [search-outside-map] - # spec: search-outside-map - # # Temp fix: - # browser: [chrome] + - test_name: integ_react_geo_display_map + desc: 'Display Map' + framework: react + category: geo + sample_name: [display-map] + spec: display-map + # Temp fix: + browser: [chrome] + - test_name: integ_react_geo_search_outside_map + desc: 'Search Outside Map' + framework: react + category: geo + sample_name: [search-outside-map] + spec: search-outside-map + # Temp fix: + browser: [chrome] # - test_name: integ_javascript_geo_display_map # desc: 'Display Map' # framework: javascript @@ -618,48 +641,48 @@ tests: # amplifyjs_dir: true # INTERACTIONS - # - test_name: integ_react_interactions_react_interactions - # desc: 'React Interactions' - # framework: react - # category: interactions - # sample_name: [chatbot-component] - # spec: chatbot-component - # browser: *minimal_browser_list - # - test_name: integ_react_interactions_chatbot_v1 - # desc: 'Chatbot V1' - # framework: react - # category: interactions - # sample_name: [lex-test-component] - # spec: chatbot-v1 - # browser: *minimal_browser_list - # - test_name: integ_react_interactions_chatbot_v2 - # desc: 'Chatbot V2' - # framework: react - # category: interactions - # sample_name: [lex-test-component] - # spec: chatbot-v2 - # browser: *minimal_browser_list - # - test_name: integ_angular_interactions - # desc: 'Angular Interactions' - # framework: angular - # category: interactions - # sample_name: [chatbot-component] - # spec: chatbot-component - # browser: *minimal_browser_list - # - test_name: integ_vue_interactions_vue_2_interactions - # desc: 'Vue 2 Interactions' - # framework: vue - # category: interactions - # sample_name: [chatbot-component] - # spec: chatbot-component - # browser: [chrome] - # - test_name: integ_vue_interactionsvue_3_interactions - # desc: 'Vue 3 Interactions' - # framework: vue - # category: interactions - # sample_name: [chatbot-component-vue3] - # spec: chatbot-component - # browser: [chrome] + - test_name: integ_react_interactions_react_interactions + desc: 'React Interactions' + framework: react + category: interactions + sample_name: [chatbot-component] + spec: chatbot-component + browser: *minimal_browser_list + - test_name: integ_react_interactions_chatbot_v1 + desc: 'Chatbot V1' + framework: react + category: interactions + sample_name: [lex-test-component] + spec: chatbot-v1 + browser: *minimal_browser_list + - test_name: integ_react_interactions_chatbot_v2 + desc: 'Chatbot V2' + framework: react + category: interactions + sample_name: [lex-test-component] + spec: chatbot-v2 + browser: *minimal_browser_list + - test_name: integ_angular_interactions + desc: 'Angular Interactions' + framework: angular + category: interactions + sample_name: [chatbot-component] + spec: chatbot-component + browser: *minimal_browser_list + - test_name: integ_vue_interactions_vue_2_interactions + desc: 'Vue 2 Interactions' + framework: vue + category: interactions + sample_name: [chatbot-component] + spec: chatbot-component + browser: [chrome] + - test_name: integ_vue_interactionsvue_3_interactions + desc: 'Vue 3 Interactions' + framework: vue + category: interactions + sample_name: [chatbot-component-vue3] + spec: chatbot-component + browser: [chrome] # PREDICTIONS # - test_name: integ_react_predictions diff --git a/.github/workflows/callable-e2e-tests.yml b/.github/workflows/callable-e2e-tests.yml index c75c0e01d53..d03744b2b06 100644 --- a/.github/workflows/callable-e2e-tests.yml +++ b/.github/workflows/callable-e2e-tests.yml @@ -43,32 +43,32 @@ jobs: timeout_minutes: ${{ matrix.integ-config.timeout_minutes || 35 }} retry_count: ${{ matrix.integ-config.retry_count || 3 }} - # e2e-test-runner-headless: - # name: E2E test runnner_headless - # needs: e2e-prep - # secrets: inherit - # strategy: - # matrix: - # integ-config: ${{ fromJson(needs.e2e-prep.outputs.integ-config-headless) }} - # fail-fast: false - # uses: ./.github/workflows/callable-e2e-test-headless.yml - # with: - # test_name: ${{ matrix.integ-config.test_name }} - # category: ${{ matrix.integ-config.category }} - # spec: ${{ matrix.integ-config.spec || '' }} - # timeout_minutes: ${{ matrix.integ-config.timeout_minutes || 35 }} - # retry_count: ${{ matrix.integ-config.retry_count || 3 }} + e2e-test-runner-headless: + name: E2E test runnner_headless + needs: e2e-prep + secrets: inherit + strategy: + matrix: + integ-config: ${{ fromJson(needs.e2e-prep.outputs.integ-config-headless) }} + fail-fast: false + uses: ./.github/workflows/callable-e2e-test-headless.yml + with: + test_name: ${{ matrix.integ-config.test_name }} + category: ${{ matrix.integ-config.category }} + spec: ${{ matrix.integ-config.spec || '' }} + timeout_minutes: ${{ matrix.integ-config.timeout_minutes || 35 }} + retry_count: ${{ matrix.integ-config.retry_count || 3 }} - # detox-e2e-test-runner: - # name: E2E test runner - # needs: e2e-prep - # strategy: - # matrix: - # integ-config: ${{ fromJson(needs.e2e-prep.outputs.detox-integ-config) }} - # fail-fast: false - # secrets: inherit - # uses: ./.github/workflows/callable-e2e-test-detox.yml - # with: - # test_name: ${{ matrix.integ-config.test_name }} - # working_directory: ${{ matrix.integ-config.working_directory }} - # timeout_minutes: ${{ matrix.integ-config.timeout_minutes || 45 }} + detox-e2e-test-runner: + name: E2E test runner + needs: e2e-prep + strategy: + matrix: + integ-config: ${{ fromJson(needs.e2e-prep.outputs.detox-integ-config) }} + fail-fast: false + secrets: inherit + uses: ./.github/workflows/callable-e2e-test-detox.yml + with: + test_name: ${{ matrix.integ-config.test_name }} + working_directory: ${{ matrix.integ-config.working_directory }} + timeout_minutes: ${{ matrix.integ-config.timeout_minutes || 45 }} diff --git a/.vscode/launch.json b/.vscode/launch.json index a0e1103e226..f3bebad16f8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -15,7 +15,7 @@ "--inspect-brk", "${workspaceRoot}/node_modules/.bin/jest", // Optionally specify a single test file to run/debug: - "sync.test.ts", + "subscription.test.ts", "--runInBand", "--testTimeout", "600000", // 10 min timeout so jest doesn't error while we're stepping through code diff --git a/lerna.json b/lerna.json index 66195d921dd..ed0d37b24f2 100644 --- a/lerna.json +++ b/lerna.json @@ -13,6 +13,9 @@ "packages/api-rest", "packages/api-graphql", "packages/datastore", + "packages/datastore-storage-adapter", + "packages/interactions", + "packages/pubsub", "packages/rtn-web-browser", "packages/notifications", "packages/rtn-push-notification", diff --git a/package.json b/package.json index 9881860411d..ced53f34bd7 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,9 @@ "packages/api-graphql", "packages/api", "packages/datastore", + "packages/datastore-storage-adapter", + "packages/interactions", + "packages/pubsub", "packages/notifications", "packages/rtn-push-notification", "packages/aws-amplify", diff --git a/packages/adapter-nextjs/__tests__/utils/createCookieStorageAdapterFromNextServerContext.test.ts b/packages/adapter-nextjs/__tests__/utils/createCookieStorageAdapterFromNextServerContext.test.ts index ba506bd3dc2..44d2509459f 100644 --- a/packages/adapter-nextjs/__tests__/utils/createCookieStorageAdapterFromNextServerContext.test.ts +++ b/packages/adapter-nextjs/__tests__/utils/createCookieStorageAdapterFromNextServerContext.test.ts @@ -32,11 +32,15 @@ describe('createCookieStorageAdapterFromNextServerContext', () => { delete: jest.fn(), }; + const mockKey = 'key'; + const mockKeyWithEncoding = 'test%40email.com'; + const mockValue = 'fabCookie'; + beforeEach(() => { jest.resetAllMocks(); }); - it('it should return cookieStorageAdapter from NextRequest and NextResponse', () => { + describe('cookieStorageAdapter created from NextRequest and NextResponse', () => { const request = new NextRequest(new URL('https://example.com')); const response = NextResponse.next(); @@ -62,22 +66,56 @@ describe('createCookieStorageAdapterFromNextServerContext', () => { } as any; const result = createCookieStorageAdapterFromNextServerContext(mockContext); - const mockKey = 'key'; - const mockValue = 'cookieName=value'; - result.get(mockKey); - expect(mockGetFunc).toHaveBeenCalledWith(mockKey); - result.getAll(); - expect(mockGetAllFunc).toHaveBeenCalled(); + it('gets cookie by calling `get` method of the underlying cookie store', () => { + result.get(mockKey); + expect(mockGetFunc).toHaveBeenCalledWith(mockKey); + }); + + it('gets cookie by calling `get` method of the underlying cookie store with a encoded cookie name', () => { + result.get(mockKeyWithEncoding); + expect(mockGetFunc).toHaveBeenCalledWith( + encodeURIComponent(mockKeyWithEncoding) + ); + }); - result.set(mockKey, mockValue); - expect(mockSetFunc).toHaveBeenCalledWith(mockKey, mockValue); + it('gets all cookies by calling `getAll` method of the underlying cookie store', () => { + result.getAll(); + expect(mockGetAllFunc).toHaveBeenCalled(); + }); + + it('sets cookie by calling the `set` method of the underlying cookie store', () => { + result.set(mockKey, mockValue); + expect(mockSetFunc).toHaveBeenCalledWith( + mockKey, + mockValue, + undefined /* didn't specify the options param in the call */ + ); + }); - result.delete(mockKey); - expect(mockDeleteFunc).toHaveBeenCalledWith(mockKey); + it('sets cookie by calling the `set` method of the underlying cookie store with a encoded cookie name', () => { + result.set(mockKeyWithEncoding, mockValue, { sameSite: 'lax' }); + expect(mockSetFunc).toHaveBeenCalledWith( + encodeURIComponent(mockKeyWithEncoding), + mockValue, + { sameSite: 'lax' } + ); + }); + + it('deletes cookie by calling the `delete` method of the underlying cookie store', () => { + result.delete(mockKey); + expect(mockDeleteFunc).toHaveBeenCalledWith(mockKey); + }); + + it('deletes cookie by calling the `delete` method of the underlying cookie store with a encoded cookie name', () => { + result.delete(mockKeyWithEncoding); + expect(mockDeleteFunc).toHaveBeenCalledWith( + encodeURIComponent(mockKeyWithEncoding) + ); + }); }); - it('should return cookieStorageAdapter from NextRequest and Response', () => { + describe('cookieStorageAdapter created from NextRequest and Response', () => { const request = new NextRequest(new URL('https://example.com')); const response = new Response(); @@ -100,16 +138,6 @@ describe('createCookieStorageAdapterFromNextServerContext', () => { response, } as any; - const result = createCookieStorageAdapterFromNextServerContext(mockContext); - const mockKey = 'key'; - const mockValue = '123'; - - result.get(mockKey); - expect(mockGetFunc).toHaveBeenCalledWith(mockKey); - - result.getAll(); - expect(mockGetAllFunc).toHaveBeenCalled(); - const mockSerializeOptions = { domain: 'example.com', expires: new Date('2023-08-22'), @@ -117,108 +145,250 @@ describe('createCookieStorageAdapterFromNextServerContext', () => { httpOnly: true, secure: true, }; - result.set(mockKey, mockValue, mockSerializeOptions); - expect(mockAppend).toHaveBeenCalledWith( - 'Set-Cookie', - `${mockKey}=${mockValue};Domain=${ - mockSerializeOptions.domain - };Expires=${mockSerializeOptions.expires.toUTCString()};HttpOnly;SameSite=${ - mockSerializeOptions.sameSite - };Secure` - ); - result.set(mockKey, mockValue, undefined); - expect(mockAppend).toHaveBeenCalledWith( - 'Set-Cookie', - `${mockKey}=${mockValue};` - ); + const result = createCookieStorageAdapterFromNextServerContext(mockContext); - result.set(mockKey, mockValue, { - httpOnly: false, - sameSite: false, - secure: false, + it('gets cookie by calling `get` method of the underlying cookie store', () => { + result.get(mockKey); + expect(mockGetFunc).toHaveBeenCalledWith(mockKey); }); - expect(mockAppend).toHaveBeenCalledWith( - 'Set-Cookie', - `${mockKey}=${mockValue};` - ); - result.delete(mockKey); - expect(mockAppend).toHaveBeenCalledWith( - 'Set-Cookie', - `${mockKey}=;Expires=${DATE_IN_THE_PAST.toUTCString()}` - ); - }); + it('gets cookie by calling `get` method of the underlying cookie store with a encoded cookie name', () => { + result.get(mockKeyWithEncoding); + expect(mockGetFunc).toHaveBeenCalledWith( + encodeURIComponent(mockKeyWithEncoding) + ); + }); - it('should return cookieStorageAdapter from Next cookies function', () => { - mockNextCookiesFunc.mockReturnValueOnce(mockNextCookiesFuncReturn); + it('gets all cookies by calling `getAll` method of the underlying cookie store', () => { + result.getAll(); + expect(mockGetAllFunc).toHaveBeenCalled(); + }); - const result = createCookieStorageAdapterFromNextServerContext({ cookies }); + it('sets cookie by calling the `set` method of the underlying cookie store with options', () => { + result.set(mockKey, mockValue, mockSerializeOptions); + expect(mockAppend).toHaveBeenCalledWith( + 'Set-Cookie', + `${mockKey}=${mockValue};Domain=${ + mockSerializeOptions.domain + };Expires=${mockSerializeOptions.expires.toUTCString()};HttpOnly;SameSite=${ + mockSerializeOptions.sameSite + };Secure` + ); + }); - const mockKey = 'key'; - const mockValue = '123'; + it('sets cookie by calling the `set` method of the underlying cookie store with options and a encoded cookie name', () => { + result.set(mockKeyWithEncoding, mockValue, mockSerializeOptions); + expect(mockAppend).toHaveBeenCalledWith( + 'Set-Cookie', + `${encodeURIComponent(mockKeyWithEncoding)}=${mockValue};Domain=${ + mockSerializeOptions.domain + };Expires=${mockSerializeOptions.expires.toUTCString()};HttpOnly;SameSite=${ + mockSerializeOptions.sameSite + };Secure` + ); + }); - result.get(mockKey); - expect(mockNextCookiesFuncReturn.get).toHaveBeenCalledWith(mockKey); + it('sets cookie by calling the `set` method of the underlying cookie store without options', () => { + result.set(mockKey, mockValue, undefined); + expect(mockAppend).toHaveBeenCalledWith( + 'Set-Cookie', + `${mockKey}=${mockValue};` + ); + }); - result.getAll(); - expect(mockNextCookiesFuncReturn.getAll).toHaveBeenCalled(); + it('sets cookie by calling the `set` method of the underlying cookie store with options that do not need to be serialized', () => { + result.set(mockKey, mockValue, { + httpOnly: false, + sameSite: false, + secure: false, + }); + expect(mockAppend).toHaveBeenCalledWith( + 'Set-Cookie', + `${mockKey}=${mockValue};` + ); + }); - result.set(mockKey, mockValue); - expect(mockNextCookiesFuncReturn.set).toHaveBeenCalledWith( - mockKey, - mockValue, - undefined - ); + it('deletes cookie by calling the `delete` method of the underlying cookie store', () => { + result.delete(mockKey); + expect(mockAppend).toHaveBeenCalledWith( + 'Set-Cookie', + `${mockKey}=;Expires=${DATE_IN_THE_PAST.toUTCString()}` + ); + }); - result.delete(mockKey); - expect(mockNextCookiesFuncReturn.delete).toHaveBeenCalledWith(mockKey); + it('deletes cookie by calling the `delete` method of the underlying cookie store with a encoded cookie name', () => { + result.delete(mockKeyWithEncoding); + expect(mockAppend).toHaveBeenCalledWith( + 'Set-Cookie', + `${encodeURIComponent( + mockKeyWithEncoding + )}=;Expires=${DATE_IN_THE_PAST.toUTCString()}` + ); + }); }); - it('should return cookieStorageAdapter from IncomingMessage and ServerResponse as the Pages Router context', () => { - const mockCookies = { - key1: 'value1', - key2: 'value2', - }; + describe('cookieStorageAdapter created from Next cookies function', () => { + mockNextCookiesFunc.mockReturnValueOnce(mockNextCookiesFuncReturn); - const request = new IncomingMessage(new Socket()); - const response = new ServerResponse(request); - const setHeaderSpy = jest.spyOn(response, 'setHeader'); + const result = createCookieStorageAdapterFromNextServerContext({ cookies }); - Object.defineProperty(request, 'cookies', { - get() { - return mockCookies; - }, + it('gets cookie by calling `get` method of the underlying cookie store', () => { + result.get(mockKey); + expect(mockNextCookiesFuncReturn.get).toHaveBeenCalledWith(mockKey); }); - const result = createCookieStorageAdapterFromNextServerContext({ - request: request as any, - response, + it('gets cookie by calling `get` method of the underlying cookie store with a encoded cookie name', () => { + result.get(mockKeyWithEncoding); + expect(mockNextCookiesFuncReturn.get).toHaveBeenCalledWith( + encodeURIComponent(mockKeyWithEncoding) + ); }); - expect(result.get('key1')).toEqual({ name: 'key1', value: 'value1' }); - expect(result.get('non-exist')).toBeUndefined(); - expect(result.getAll()).toEqual([ - { name: 'key1', value: 'value1' }, - { name: 'key2', value: 'value2' }, - ]); + it('gets all cookies by calling `getAll` method of the underlying cookie store', () => { + result.getAll(); + expect(mockNextCookiesFuncReturn.getAll).toHaveBeenCalled(); + }); - result.set('key3', 'value3'); - expect(setHeaderSpy).toHaveBeenCalledWith('Set-Cookie', 'key3=value3;'); + it('sets cookie by calling the `set` method of the underlying cookie store', () => { + result.set(mockKey, mockValue); + expect(mockNextCookiesFuncReturn.set).toHaveBeenCalledWith( + mockKey, + mockValue, + undefined + ); + }); - result.set('key4', 'value4', { - httpOnly: true, + it('sets cookie by calling the `set` method of the underlying cookie store with a encoded cookie name', () => { + result.set(mockKeyWithEncoding, mockValue); + expect(mockNextCookiesFuncReturn.set).toHaveBeenCalledWith( + encodeURIComponent(mockKeyWithEncoding), + mockValue, + undefined + ); }); - expect(setHeaderSpy).toHaveBeenCalledWith( - 'Set-Cookie', - 'key4=value4;HttpOnly' - ); - result.delete('key3'); - expect(setHeaderSpy).toHaveBeenCalledWith( - 'Set-Cookie', - `key3=;Expires=${DATE_IN_THE_PAST.toUTCString()}` - ); + it('deletes cookie by calling the `delete` method of the underlying cookie store', () => { + result.delete(mockKey); + expect(mockNextCookiesFuncReturn.delete).toHaveBeenCalledWith(mockKey); + }); + + it('deletes cookie by calling the `delete` method of the underlying cookie store with a encoded cookie name', () => { + result.delete(mockKeyWithEncoding); + expect(mockNextCookiesFuncReturn.delete).toHaveBeenCalledWith( + encodeURIComponent(mockKeyWithEncoding) + ); + }); + }); + + describe('cookieStorageAdapter created from IncomingMessage and ServerResponse as the Pages Router context', () => { + it('operates with the underlying cookie store', () => { + const mockCookies = { + key1: 'value1', + key2: 'value2', + }; + + const request = new IncomingMessage(new Socket()); + const response = new ServerResponse(request); + const setHeaderSpy = jest.spyOn(response, 'setHeader'); + + Object.defineProperty(request, 'cookies', { + get() { + return mockCookies; + }, + }); + + const result = createCookieStorageAdapterFromNextServerContext({ + request: request as any, + response, + }); + + expect(result.get('key1')).toEqual({ name: 'key1', value: 'value1' }); + expect(result.get('non-exist')).toBeUndefined(); + expect(result.getAll()).toEqual([ + { name: 'key1', value: 'value1' }, + { name: 'key2', value: 'value2' }, + ]); + + result.set('key3', 'value3'); + expect(setHeaderSpy).toHaveBeenCalledWith('Set-Cookie', 'key3=value3;'); + + result.set('key4', 'value4', { + httpOnly: true, + }); + expect(setHeaderSpy).toHaveBeenCalledWith( + 'Set-Cookie', + 'key4=value4;HttpOnly' + ); + + result.delete('key3'); + expect(setHeaderSpy).toHaveBeenCalledWith( + 'Set-Cookie', + `key3=;Expires=${DATE_IN_THE_PAST.toUTCString()}` + ); + }); + + it('operates with the underlying cookie store with encoded cookie names', () => { + // these the auth keys generated by Amplify + const encodedCookieName1 = encodeURIComponent('test@email.com.idToken'); + const encodedCookieName2 = encodeURIComponent( + 'test@email.com.refreshToken' + ); + + const mockCookies = { + // these keys are generate by js-cookie used on the client side + [encodeURIComponent(encodedCookieName1)]: 'value1', + [encodeURIComponent(encodedCookieName2)]: 'value2', + }; + + const request = new IncomingMessage(new Socket()); + const response = new ServerResponse(request); + const setHeaderSpy = jest.spyOn(response, 'setHeader'); + + Object.defineProperty(request, 'cookies', { + get() { + return mockCookies; + }, + }); + + const result = createCookieStorageAdapterFromNextServerContext({ + request: request as any, + response, + }); + + expect(result.get(encodedCookieName1)).toEqual({ + name: encodedCookieName1, + value: 'value1', + }); + expect(result.get('non-exist')).toBeUndefined(); + expect(result.getAll()).toEqual([ + // these keys are generate by js-cookie used on the client side + { name: encodeURIComponent(encodedCookieName1), value: 'value1' }, + { name: encodeURIComponent(encodedCookieName2), value: 'value2' }, + ]); + + result.set('key3', 'value3'); + expect(setHeaderSpy).toHaveBeenCalledWith('Set-Cookie', 'key3=value3;'); + + result.set('key4', 'value4', { + httpOnly: true, + }); + + const encodedCookieName = encodeURIComponent( + 'test@email.com.somethingElse' + ); + result.set(encodeURIComponent('test@email.com.somethingElse'), 'value5'); + expect(setHeaderSpy).toHaveBeenCalledWith( + 'Set-Cookie', + `${encodeURIComponent(encodedCookieName)}=value5;` + ); + + result.delete('key3'); + expect(setHeaderSpy).toHaveBeenCalledWith( + 'Set-Cookie', + `key3=;Expires=${DATE_IN_THE_PAST.toUTCString()}` + ); + }); }); it('should throw error when no cookie storage adapter is created from the context', () => { diff --git a/packages/adapter-nextjs/src/utils/createCookieStorageAdapterFromNextServerContext.ts b/packages/adapter-nextjs/src/utils/createCookieStorageAdapterFromNextServerContext.ts index 51fe055a765..d30b7ba7859 100644 --- a/packages/adapter-nextjs/src/utils/createCookieStorageAdapterFromNextServerContext.ts +++ b/packages/adapter-nextjs/src/utils/createCookieStorageAdapterFromNextServerContext.ts @@ -78,10 +78,16 @@ const createCookieStorageAdapterFromNextRequestAndNextResponse = ( const mutableCookieStore = response.cookies; return { - get: readonlyCookieStore.get.bind(readonlyCookieStore), + get(name) { + return readonlyCookieStore.get(ensureEncodedForJSCookie(name)); + }, getAll: readonlyCookieStore.getAll.bind(readonlyCookieStore), - set: mutableCookieStore.set.bind(mutableCookieStore), - delete: mutableCookieStore.delete.bind(mutableCookieStore), + set(name, value, options) { + mutableCookieStore.set(ensureEncodedForJSCookie(name), value, options); + }, + delete(name) { + mutableCookieStore.delete(ensureEncodedForJSCookie(name)); + }, }; }; @@ -95,7 +101,9 @@ const createCookieStorageAdapterFromNextRequestAndHttpResponse = ( ); return { - get: readonlyCookieStore.get.bind(readonlyCookieStore), + get(name) { + return readonlyCookieStore.get(ensureEncodedForJSCookie(name)); + }, getAll: readonlyCookieStore.getAll.bind(readonlyCookieStore), ...mutableCookieStore, }; @@ -113,7 +121,7 @@ const createCookieStorageAdapterFromNextCookies = ( // and safely ignore the error if it is thrown. const setFunc: CookieStorage.Adapter['set'] = (name, value, options) => { try { - cookieStore.set(name, value, options); + cookieStore.set(ensureEncodedForJSCookie(name), value, options); } catch { // no-op } @@ -121,14 +129,16 @@ const createCookieStorageAdapterFromNextCookies = ( const deleteFunc: CookieStorage.Adapter['delete'] = name => { try { - cookieStore.delete(name); + cookieStore.delete(ensureEncodedForJSCookie(name)); } catch { // no-op } }; return { - get: cookieStore.get.bind(cookieStore), + get(name) { + return cookieStore.get(ensureEncodedForJSCookie(name)); + }, getAll: cookieStore.getAll.bind(cookieStore), set: setFunc, delete: deleteFunc, @@ -147,7 +157,7 @@ const createCookieStorageAdapterFromGetServerSidePropsContext = ( return { get(name) { - const value = cookiesMap[name]; + const value = cookiesMap[ensureEncodedForJSCookie(name)]; return value ? { name, @@ -161,13 +171,17 @@ const createCookieStorageAdapterFromGetServerSidePropsContext = ( set(name, value, options) { response.setHeader( 'Set-Cookie', - `${name}=${value};${options ? serializeSetCookieOptions(options) : ''}` + `${ensureEncodedForJSCookie(name)}=${value};${ + options ? serializeSetCookieOptions(options) : '' + }` ); }, delete(name) { response.setHeader( 'Set-Cookie', - `${name}=;Expires=${DATE_IN_THE_PAST.toUTCString()}` + `${ensureEncodedForJSCookie( + name + )}=;Expires=${DATE_IN_THE_PAST.toUTCString()}` ); }, }; @@ -179,13 +193,17 @@ const createMutableCookieStoreFromHeaders = ( const setFunc: CookieStorage.Adapter['set'] = (name, value, options) => { headers.append( 'Set-Cookie', - `${name}=${value};${options ? serializeSetCookieOptions(options) : ''}` + `${ensureEncodedForJSCookie(name)}=${value};${ + options ? serializeSetCookieOptions(options) : '' + }` ); }; const deleteFunc: CookieStorage.Adapter['delete'] = name => { headers.append( 'Set-Cookie', - `${name}=;Expires=${DATE_IN_THE_PAST.toUTCString()}` + `${ensureEncodedForJSCookie( + name + )}=;Expires=${DATE_IN_THE_PAST.toUTCString()}` ); }; return { @@ -197,7 +215,7 @@ const createMutableCookieStoreFromHeaders = ( const serializeSetCookieOptions = ( options: CookieStorage.SetCookieOptions ): string => { - const { expires, maxAge, domain, httpOnly, sameSite, secure } = options; + const { expires, domain, httpOnly, sameSite, secure } = options; const serializedOptions: string[] = []; if (domain) { serializedOptions.push(`Domain=${domain}`); @@ -216,3 +234,12 @@ const serializeSetCookieOptions = ( } return serializedOptions.join(';'); }; + +// Ensures the cookie names are encoded in order to look up the cookie store +// that is manipulated by js-cookie on the client side. +// Details of the js-cookie encoding behavior see: +// https://github.com/js-cookie/js-cookie#encoding +// The implementation is borrowed from js-cookie without escaping `[()]` as +// we are not using those chars in the auth keys. +const ensureEncodedForJSCookie = (name: string): string => + encodeURIComponent(name).replace(/%(2[346B]|5E|60|7C)/g, decodeURIComponent); diff --git a/packages/adapter-nextjs/tsconfig.json b/packages/adapter-nextjs/tsconfig.json index 2a97edbd215..6b5ad23ba75 100755 --- a/packages/adapter-nextjs/tsconfig.json +++ b/packages/adapter-nextjs/tsconfig.json @@ -9,7 +9,7 @@ "sourceMap": true, "module": "commonjs", "moduleResolution": "node", - "allowJs": false, + "allowJs": true, "declaration": true, "typeRoots": ["./node_modules/@types", "../../node_modules/@types"], "types": ["node"], diff --git a/packages/analytics/__tests__/providers/kinesis-firehose/apis/flushEvents.test.ts b/packages/analytics/__tests__/providers/kinesis-firehose/apis/flushEvents.test.ts index d5b3629032e..14c5849c0a3 100644 --- a/packages/analytics/__tests__/providers/kinesis-firehose/apis/flushEvents.test.ts +++ b/packages/analytics/__tests__/providers/kinesis-firehose/apis/flushEvents.test.ts @@ -11,7 +11,7 @@ import { mockCredentialConfig, } from '../../../testUtils/mockConstants.test'; import { flushEvents } from '../../../../src/providers/kinesis-firehose/apis'; -import { ConsoleLogger } from '@aws-amplify/core/internals/utils'; +import { ConsoleLogger } from '@aws-amplify/core'; jest.mock('../../../../src/utils'); jest.mock('../../../../src/providers/kinesis-firehose/utils'); diff --git a/packages/analytics/__tests__/providers/kinesis-firehose/apis/record.test.ts b/packages/analytics/__tests__/providers/kinesis-firehose/apis/record.test.ts index c66d83a0e38..c7e5e8cca5a 100644 --- a/packages/analytics/__tests__/providers/kinesis-firehose/apis/record.test.ts +++ b/packages/analytics/__tests__/providers/kinesis-firehose/apis/record.test.ts @@ -11,7 +11,7 @@ import { mockCredentialConfig, } from '../../../testUtils/mockConstants.test'; import { record } from '../../../../src/providers/kinesis-firehose'; -import { ConsoleLogger as Logger } from '@aws-amplify/core/internals/utils'; +import { ConsoleLogger } from '@aws-amplify/core'; import { RecordInput as KinesisFirehoseRecordInput } from '../../../../src/providers/kinesis-firehose/types'; jest.mock('../../../../src/utils'); @@ -28,8 +28,8 @@ describe('Analytics KinesisFirehose API: record', () => { const mockIsAnalyticsEnabled = isAnalyticsEnabled as jest.Mock; const mockGetEventBuffer = getEventBuffer as jest.Mock; const mockAppend = jest.fn(); - const loggerWarnSpy = jest.spyOn(Logger.prototype, 'warn'); - const loggerDebugSpy = jest.spyOn(Logger.prototype, 'debug'); + const loggerWarnSpy = jest.spyOn(ConsoleLogger.prototype, 'warn'); + const loggerDebugSpy = jest.spyOn(ConsoleLogger.prototype, 'debug'); beforeEach(() => { mockIsAnalyticsEnabled.mockReturnValue(true); diff --git a/packages/analytics/__tests__/providers/kinesis/apis/flushEvents.test.ts b/packages/analytics/__tests__/providers/kinesis/apis/flushEvents.test.ts index 75aaef41a4e..2a0a3d15c3d 100644 --- a/packages/analytics/__tests__/providers/kinesis/apis/flushEvents.test.ts +++ b/packages/analytics/__tests__/providers/kinesis/apis/flushEvents.test.ts @@ -9,7 +9,7 @@ import { } from '../../../testUtils/mockConstants.test'; import { getEventBuffer } from '../../../../src/providers/kinesis/utils/getEventBuffer'; import { flushEvents } from '../../../../src/providers/kinesis/apis'; -import { ConsoleLogger } from '@aws-amplify/core/internals/utils'; +import { ConsoleLogger } from '@aws-amplify/core'; jest.mock('../../../../src/utils'); jest.mock('../../../../src/providers/kinesis/utils/getEventBuffer'); diff --git a/packages/analytics/__tests__/providers/kinesis/apis/record.test.ts b/packages/analytics/__tests__/providers/kinesis/apis/record.test.ts index e90ef762283..43a40f6052d 100644 --- a/packages/analytics/__tests__/providers/kinesis/apis/record.test.ts +++ b/packages/analytics/__tests__/providers/kinesis/apis/record.test.ts @@ -9,7 +9,7 @@ import { mockCredentialConfig, } from '../../../testUtils/mockConstants.test'; import { record } from '../../../../src/providers/kinesis'; -import { ConsoleLogger as Logger } from '@aws-amplify/core/internals/utils'; +import { ConsoleLogger } from '@aws-amplify/core'; import { RecordInput as KinesisRecordInput } from '../../../../src/providers/kinesis/types'; jest.mock('../../../../src/utils'); @@ -28,8 +28,8 @@ describe('Analytics Kinesis API: record', () => { const mockGetEventBuffer = getEventBuffer as jest.Mock; const mockIsAnalyticsEnabled = isAnalyticsEnabled as jest.Mock; const mockAppend = jest.fn(); - const loggerWarnSpy = jest.spyOn(Logger.prototype, 'warn'); - const loggerDebugSpy = jest.spyOn(Logger.prototype, 'debug'); + const loggerWarnSpy = jest.spyOn(ConsoleLogger.prototype, 'warn'); + const loggerDebugSpy = jest.spyOn(ConsoleLogger.prototype, 'debug'); beforeEach(() => { mockIsAnalyticsEnabled.mockReturnValue(true); diff --git a/packages/analytics/__tests__/providers/personalize/apis/flushEvents.test.ts b/packages/analytics/__tests__/providers/personalize/apis/flushEvents.test.ts index eff60b3f93b..5ae5e67c58b 100644 --- a/packages/analytics/__tests__/providers/personalize/apis/flushEvents.test.ts +++ b/packages/analytics/__tests__/providers/personalize/apis/flushEvents.test.ts @@ -11,7 +11,7 @@ import { mockPersonalizeConfig, } from '../../../testUtils/mockConstants.test'; import { flushEvents } from '../../../../src/providers/personalize'; -import { ConsoleLogger } from '@aws-amplify/core/internals/utils'; +import { ConsoleLogger } from '@aws-amplify/core'; jest.mock('../../../../src/utils'); jest.mock('../../../../src/providers/personalize/utils'); diff --git a/packages/analytics/__tests__/providers/personalize/apis/record.test.ts b/packages/analytics/__tests__/providers/personalize/apis/record.test.ts index bf044149c4e..b203c4e141e 100644 --- a/packages/analytics/__tests__/providers/personalize/apis/record.test.ts +++ b/packages/analytics/__tests__/providers/personalize/apis/record.test.ts @@ -13,7 +13,7 @@ import { mockPersonalizeConfig, } from '../../../testUtils/mockConstants.test'; import { record } from '../../../../src/providers/personalize'; -import { ConsoleLogger as Logger } from '@aws-amplify/core/internals/utils'; +import { ConsoleLogger } from '@aws-amplify/core'; import { RecordInput as PersonalizeRecordInput } from '../../../../src/providers/personalize/types'; import { IDENTIFY_EVENT_TYPE, @@ -45,8 +45,8 @@ describe('Analytics Personalize API: record', () => { const mockAutoTrackMedia = autoTrackMedia as jest.Mock; const mockGetEventBuffer = getEventBuffer as jest.Mock; const mockAppend = jest.fn(); - const loggerWarnSpy = jest.spyOn(Logger.prototype, 'warn'); - const loggerDebugSpy = jest.spyOn(Logger.prototype, 'debug'); + const loggerWarnSpy = jest.spyOn(ConsoleLogger.prototype, 'warn'); + const loggerDebugSpy = jest.spyOn(ConsoleLogger.prototype, 'debug'); const mockEventBuffer = { append: mockAppend, }; diff --git a/packages/analytics/__tests__/providers/personalize/utils/cachedSession.test.ts b/packages/analytics/__tests__/providers/personalize/utils/cachedSession.test.ts index fbc7516fa18..ec6bc5aa1da 100644 --- a/packages/analytics/__tests__/providers/personalize/utils/cachedSession.test.ts +++ b/packages/analytics/__tests__/providers/personalize/utils/cachedSession.test.ts @@ -1,8 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Cache, BrowserStorageCache } from '@aws-amplify/core'; -import { isBrowser } from '@aws-amplify/core/internals/utils'; +import { Cache } from '@aws-amplify/core'; +import { isBrowser, amplifyUuid } from '@aws-amplify/core/internals/utils'; import { resolveCachedSession, updateCachedSession, @@ -11,11 +11,14 @@ import { jest.mock('@aws-amplify/core'); jest.mock('@aws-amplify/core/internals/utils'); +const mockAmplifyUuid = amplifyUuid as jest.Mock; + describe('Analytics service provider Personalize utils: cachedSession', () => { const sessionIdCacheKey = '_awsct_sid.personalize'; const userIdCacheKey = '_awsct_uid.personalize'; - const mockCache = Cache as jest.Mocked; + const mockCache = Cache as jest.Mocked; const mockIsBrowser = isBrowser as jest.Mock; + const mockUuid = 'b2bd676e-bc6b-40f4-bd86-1e31a07f7d10'; const mockSession = { sessionId: 'sessionId0', @@ -30,6 +33,7 @@ describe('Analytics service provider Personalize utils: cachedSession', () => { beforeEach(() => { mockCache.getItem.mockImplementation(key => mockCachedStorage[key]); mockIsBrowser.mockReturnValue(false); + mockAmplifyUuid.mockReturnValue(mockUuid); }); afterEach(() => { @@ -38,16 +42,16 @@ describe('Analytics service provider Personalize utils: cachedSession', () => { mockCache.setItem.mockReset(); }); - it('resolve cached session from Cache', () => { - const result = resolveCachedSession('trackingId0'); + it('resolve cached session from Cache', async () => { + const result = await resolveCachedSession(); expect(result).toStrictEqual(mockSession); }); - it('create a new session if there is no cache', () => { - mockCache.getItem.mockImplementation(() => undefined); - const result = resolveCachedSession('trackingId0'); + it('create a new session if there is no cache', async () => { + mockCache.getItem.mockImplementation(async () => undefined); + const result = await resolveCachedSession(); expect(result.sessionId).not.toBe(mockSession.sessionId); - expect(result.sessionId.length).toBeGreaterThan(0); + expect(result.sessionId).toEqual(mockUuid); expect(result.userId).toBe(undefined); }); diff --git a/packages/analytics/__tests__/providers/pinpoint/apis/configureAutoTrack.test.ts b/packages/analytics/__tests__/providers/pinpoint/apis/configureAutoTrack.test.ts new file mode 100644 index 00000000000..90664582e73 --- /dev/null +++ b/packages/analytics/__tests__/providers/pinpoint/apis/configureAutoTrack.test.ts @@ -0,0 +1,160 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { ConfigureAutoTrackInput } from '../../../../src/providers/pinpoint'; +import { + EventTracker, + PageViewTracker, + SessionTracker, +} from '../../../../src/trackers'; + +jest.mock('../../../../src/trackers'); + +const MOCK_INPUT = { + enable: true, + type: 'event', + options: { + attributes: { + 'custom-attr': 'val', + }, + }, +} as ConfigureAutoTrackInput; + +describe('Pinpoint API: configureAutoTrack', () => { + const MockEventTracker = EventTracker as jest.MockedClass< + typeof EventTracker + >; + const MockPageViewTracker = PageViewTracker as jest.MockedClass< + typeof PageViewTracker + >; + const MockSessionTracker = SessionTracker as jest.MockedClass< + typeof SessionTracker + >; + + beforeEach(() => { + MockEventTracker.mockClear(); + MockPageViewTracker.mockClear(); + MockSessionTracker.mockClear(); + }); + + it('Validates the tracker configuration', () => { + expect.assertions(1); + + jest.isolateModules(() => { + const { + configureAutoTrack, + } = require('../../../../src/providers/pinpoint/apis'); + + try { + configureAutoTrack({ + ...MOCK_INPUT, + type: 'invalidTracker', + } as any); + } catch (e) { + expect(e.message).toBe('Invalid tracker type specified.'); + } + }); + }); + + it('Creates a new Event tracker when required', () => { + jest.isolateModules(() => { + const { + configureAutoTrack, + } = require('../../../../src/providers/pinpoint/apis'); + + configureAutoTrack(MOCK_INPUT); + }); + + expect(MockEventTracker).toBeCalledWith( + expect.any(Function), + MOCK_INPUT.options + ); + }); + + it('Creates a new Session tracker when required', () => { + const testInput = { + ...MOCK_INPUT, + type: 'session', + } as ConfigureAutoTrackInput; + + jest.isolateModules(() => { + const { + configureAutoTrack, + } = require('../../../../src/providers/pinpoint/apis'); + + configureAutoTrack(testInput); + }); + + expect(MockSessionTracker).toBeCalledWith( + expect.any(Function), + testInput.options + ); + }); + + it('Creates a new PageView tracker when required', () => { + const testInput = { + ...MOCK_INPUT, + type: 'pageView', + } as ConfigureAutoTrackInput; + + jest.isolateModules(() => { + const { + configureAutoTrack, + } = require('../../../../src/providers/pinpoint/apis'); + + configureAutoTrack(testInput); + }); + + expect(MockPageViewTracker).toBeCalledWith( + expect.any(Function), + testInput.options + ); + }); + + it('Reconfigures an existing tracker', () => { + jest.isolateModules(() => { + const { + configureAutoTrack, + } = require('../../../../src/providers/pinpoint/apis'); + + // Enable the tracker + configureAutoTrack(MOCK_INPUT); + expect(MockEventTracker).toBeCalledWith( + expect.any(Function), + MOCK_INPUT.options + ); + + // Reconfigure the tracker + configureAutoTrack(MOCK_INPUT); + expect( + MockEventTracker.mock.instances[0].configure + ).toHaveBeenCalledTimes(1); + }); + }); + + it("Cleans up a tracker when it's disabled", () => { + const testInput = { + ...MOCK_INPUT, + enable: false, + } as ConfigureAutoTrackInput; + + jest.isolateModules(() => { + const { + configureAutoTrack, + } = require('../../../../src/providers/pinpoint/apis'); + + // Enable the tracker + configureAutoTrack(MOCK_INPUT); + expect(MockEventTracker).toBeCalledWith( + expect.any(Function), + MOCK_INPUT.options + ); + + // Disable the tracker + configureAutoTrack(testInput); + expect(MockEventTracker.mock.instances[0].cleanup).toHaveBeenCalledTimes( + 1 + ); + }); + }); +}); diff --git a/packages/analytics/__tests__/providers/pinpoint/apis/flushEvents.test.ts b/packages/analytics/__tests__/providers/pinpoint/apis/flushEvents.test.ts index 4a7fd733092..ace1313da1a 100644 --- a/packages/analytics/__tests__/providers/pinpoint/apis/flushEvents.test.ts +++ b/packages/analytics/__tests__/providers/pinpoint/apis/flushEvents.test.ts @@ -8,10 +8,8 @@ import { import { config, credentials, identityId } from './testUtils/data'; import { flushEvents } from '../../../../src/providers/pinpoint'; import { flushEvents as pinpointFlushEvents } from '@aws-amplify/core/internals/providers/pinpoint'; -import { - AnalyticsAction, - ConsoleLogger, -} from '@aws-amplify/core/internals/utils'; +import { AnalyticsAction } from '@aws-amplify/core/internals/utils'; +import { ConsoleLogger } from '@aws-amplify/core'; import { getAnalyticsUserAgentString } from '../../../../src/utils'; jest.mock('../../../../src/providers/pinpoint/utils'); diff --git a/packages/analytics/__tests__/providers/pinpoint/apis/identifyUser.test.ts b/packages/analytics/__tests__/providers/pinpoint/apis/identifyUser.test.ts index 5165a6a1652..57a093797d3 100644 --- a/packages/analytics/__tests__/providers/pinpoint/apis/identifyUser.test.ts +++ b/packages/analytics/__tests__/providers/pinpoint/apis/identifyUser.test.ts @@ -70,7 +70,7 @@ describe('Analytics Pinpoint Provider API: identifyUser', () => { userProfile: {}, }; const options: IdentifyUserInput['options'] = { - serviceOptions: { userAttributes }, + userAttributes, }; await identifyUser({ ...input, options }); expect(mockUpdateEndpoint).toBeCalledWith({ diff --git a/packages/analytics/__tests__/providers/pinpoint/apis/record.test.ts b/packages/analytics/__tests__/providers/pinpoint/apis/record.test.ts index 62864d6f2b9..62c2082beb4 100644 --- a/packages/analytics/__tests__/providers/pinpoint/apis/record.test.ts +++ b/packages/analytics/__tests__/providers/pinpoint/apis/record.test.ts @@ -1,6 +1,6 @@ import { Hub } from '@aws-amplify/core'; import { record as pinpointRecord } from '@aws-amplify/core/internals/providers/pinpoint'; -import { ConsoleLogger as Logger } from '@aws-amplify/core/internals/utils'; +import { ConsoleLogger } from '@aws-amplify/core'; import { record } from '../../../../src/providers/pinpoint'; import { resolveConfig, @@ -28,7 +28,7 @@ jest.mock('../../../../src/providers/pinpoint/utils'); describe('Pinpoint API: record', () => { // create spies - const loggerWarnSpy = jest.spyOn(Logger.prototype, 'warn'); + const loggerWarnSpy = jest.spyOn(ConsoleLogger.prototype, 'warn'); // create mocks const mockPinpointRecord = pinpointRecord as jest.Mock; const mockResolveConfig = resolveConfig as jest.Mock; diff --git a/packages/analytics/package.json b/packages/analytics/package.json index 6d6c0e72a47..41ce9d796a7 100644 --- a/packages/analytics/package.json +++ b/packages/analytics/package.json @@ -93,7 +93,6 @@ ], "dependencies": { "tslib": "^2.5.0", - "uuid": "^9.0.0", "@aws-sdk/client-kinesis": "3.398.0", "@aws-sdk/client-firehose": "3.398.0", "@aws-sdk/client-personalize-events": "3.398.0", @@ -104,8 +103,8 @@ }, "devDependencies": { "@aws-amplify/core": "6.0.0", + "@aws-amplify/react-native": "^1.0.0", "@aws-sdk/types": "3.398.0", - "@types/uuid": "^9.0.0", "typescript": "5.0.2" }, "jest": { diff --git a/packages/analytics/src/errors/validation.ts b/packages/analytics/src/errors/validation.ts index 65bcc89d263..5ccafeabc44 100644 --- a/packages/analytics/src/errors/validation.ts +++ b/packages/analytics/src/errors/validation.ts @@ -8,6 +8,8 @@ export enum AnalyticsValidationErrorCode { NoCredentials = 'NoCredentials', NoEventName = 'NoEventName', NoRegion = 'NoRegion', + InvalidTracker = 'InvalidTracker', + UnsupportedPlatform = 'UnsupportedPlatform', NoTrackingId = 'NoTrackingId', InvalidFlushSize = 'InvalidFlushSize', } @@ -26,6 +28,12 @@ export const validationErrorMap: AmplifyErrorMap = [AnalyticsValidationErrorCode.NoRegion]: { message: 'Missing region.', }, + [AnalyticsValidationErrorCode.InvalidTracker]: { + message: 'Invalid tracker type specified.', + }, + [AnalyticsValidationErrorCode.UnsupportedPlatform]: { + message: 'Only session tracking is supported on React Native.', + }, [AnalyticsValidationErrorCode.InvalidFlushSize]: { message: 'Invalid FlushSize, it should be smaller than BufferSize', }, diff --git a/packages/analytics/src/index.ts b/packages/analytics/src/index.ts index 7f9732ce48a..f3e9a6ade46 100644 --- a/packages/analytics/src/index.ts +++ b/packages/analytics/src/index.ts @@ -4,9 +4,11 @@ export { record, identifyUser, + configureAutoTrack, + flushEvents, RecordInput, IdentifyUserInput, - flushEvents, + ConfigureAutoTrackInput, } from './providers/pinpoint'; export { enable, disable } from './apis'; export { AnalyticsError } from './errors'; diff --git a/packages/analytics/src/providers/kinesis-firehose/apis/flushEvents.ts b/packages/analytics/src/providers/kinesis-firehose/apis/flushEvents.ts index 41a8d8adad3..981a7a8cc93 100644 --- a/packages/analytics/src/providers/kinesis-firehose/apis/flushEvents.ts +++ b/packages/analytics/src/providers/kinesis-firehose/apis/flushEvents.ts @@ -6,10 +6,8 @@ import { getAnalyticsUserAgentString, resolveCredentials, } from '../../../utils'; -import { - AnalyticsAction, - ConsoleLogger, -} from '@aws-amplify/core/internals/utils'; +import { AnalyticsAction } from '@aws-amplify/core/internals/utils'; +import { ConsoleLogger } from '@aws-amplify/core'; const logger = new ConsoleLogger('KinesisFirehose'); diff --git a/packages/analytics/src/providers/kinesis-firehose/apis/record.ts b/packages/analytics/src/providers/kinesis-firehose/apis/record.ts index 840866d34df..917dd95f66f 100644 --- a/packages/analytics/src/providers/kinesis-firehose/apis/record.ts +++ b/packages/analytics/src/providers/kinesis-firehose/apis/record.ts @@ -9,12 +9,10 @@ import { resolveCredentials, } from '../../../utils'; import { fromUtf8 } from '@smithy/util-utf8'; -import { - AnalyticsAction, - ConsoleLogger as Logger, -} from '@aws-amplify/core/internals/utils'; +import { AnalyticsAction } from '@aws-amplify/core/internals/utils'; +import { ConsoleLogger } from '@aws-amplify/core'; -const logger = new Logger('KinesisFirehose'); +const logger = new ConsoleLogger('KinesisFirehose'); export const record = ({ streamName, data }: RecordInput): void => { if (!isAnalyticsEnabled()) { diff --git a/packages/analytics/src/providers/kinesis-firehose/types/buffer.ts b/packages/analytics/src/providers/kinesis-firehose/types/buffer.ts index 405005a02b7..6ce03a616e1 100644 --- a/packages/analytics/src/providers/kinesis-firehose/types/buffer.ts +++ b/packages/analytics/src/providers/kinesis-firehose/types/buffer.ts @@ -3,7 +3,7 @@ import { EventBufferConfig } from '../../../utils'; import { KinesisStream } from '../../../types'; -import { Credentials } from '@aws-sdk/types'; +import { AWSCredentials } from '@aws-amplify/core/internals/utils'; export type KinesisFirehoseBufferEvent = KinesisStream & { event: Uint8Array; @@ -13,7 +13,7 @@ export type KinesisFirehoseBufferEvent = KinesisStream & { export type KinesisFirehoseEventBufferConfig = EventBufferConfig & { region: string; - credentials: Credentials; + credentials: AWSCredentials; identityId?: string; resendLimit?: number; userAgentValue?: string; diff --git a/packages/analytics/src/providers/kinesis/apis/flushEvents.ts b/packages/analytics/src/providers/kinesis/apis/flushEvents.ts index ed664cb9e49..2937aa355a2 100644 --- a/packages/analytics/src/providers/kinesis/apis/flushEvents.ts +++ b/packages/analytics/src/providers/kinesis/apis/flushEvents.ts @@ -7,10 +7,8 @@ import { resolveCredentials, } from '../../../utils'; import { getEventBuffer } from '../utils/getEventBuffer'; -import { - AnalyticsAction, - ConsoleLogger, -} from '@aws-amplify/core/internals/utils'; +import { AnalyticsAction } from '@aws-amplify/core/internals/utils'; +import { ConsoleLogger } from '@aws-amplify/core'; const logger = new ConsoleLogger('Kinesis'); diff --git a/packages/analytics/src/providers/kinesis/apis/record.ts b/packages/analytics/src/providers/kinesis/apis/record.ts index 77d5f91c936..dbfabab423e 100644 --- a/packages/analytics/src/providers/kinesis/apis/record.ts +++ b/packages/analytics/src/providers/kinesis/apis/record.ts @@ -10,10 +10,8 @@ import { resolveCredentials, } from '../../../utils'; import { fromUtf8 } from '@smithy/util-utf8'; -import { - AnalyticsAction, - ConsoleLogger, -} from '@aws-amplify/core/internals/utils'; +import { AnalyticsAction } from '@aws-amplify/core/internals/utils'; +import { ConsoleLogger } from '@aws-amplify/core'; const logger = new ConsoleLogger('Kinesis'); diff --git a/packages/analytics/src/providers/kinesis/types/buffer.ts b/packages/analytics/src/providers/kinesis/types/buffer.ts index b41e00cd587..57ab85ecb34 100644 --- a/packages/analytics/src/providers/kinesis/types/buffer.ts +++ b/packages/analytics/src/providers/kinesis/types/buffer.ts @@ -1,8 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { AWSCredentials } from '@aws-amplify/core/internals/utils'; import { EventBufferConfig } from '../../../utils'; -import { Credentials } from '@aws-sdk/types'; import { KinesisShard } from '../../../types'; export type KinesisBufferEvent = KinesisShard & { @@ -13,7 +13,7 @@ export type KinesisBufferEvent = KinesisShard & { export type KinesisEventBufferConfig = EventBufferConfig & { region: string; - credentials: Credentials; + credentials: AWSCredentials; identityId?: string; resendLimit?: number; userAgentValue?: string; diff --git a/packages/analytics/src/providers/personalize/apis/flushEvents.ts b/packages/analytics/src/providers/personalize/apis/flushEvents.ts index 0d1b04b328d..0b4b489df27 100644 --- a/packages/analytics/src/providers/personalize/apis/flushEvents.ts +++ b/packages/analytics/src/providers/personalize/apis/flushEvents.ts @@ -6,10 +6,8 @@ import { getAnalyticsUserAgentString, resolveCredentials, } from '../../../utils'; -import { - AnalyticsAction, - ConsoleLogger, -} from '@aws-amplify/core/internals/utils'; +import { AnalyticsAction } from '@aws-amplify/core/internals/utils'; +import { ConsoleLogger } from '@aws-amplify/core'; const logger = new ConsoleLogger('Personalize'); diff --git a/packages/analytics/src/providers/personalize/apis/record.ts b/packages/analytics/src/providers/personalize/apis/record.ts index 2d7dfa07b32..1ee479909a4 100644 --- a/packages/analytics/src/providers/personalize/apis/record.ts +++ b/packages/analytics/src/providers/personalize/apis/record.ts @@ -14,10 +14,8 @@ import { isAnalyticsEnabled, resolveCredentials, } from '../../../utils'; -import { - AnalyticsAction, - ConsoleLogger, -} from '@aws-amplify/core/internals/utils'; +import { AnalyticsAction } from '@aws-amplify/core/internals/utils'; +import { ConsoleLogger } from '@aws-amplify/core'; import { IDENTIFY_EVENT_TYPE, MEDIA_AUTO_TRACK_EVENT_TYPE, @@ -39,10 +37,10 @@ export const record = ({ const { region, trackingId, bufferSize, flushSize, flushInterval } = resolveConfig(); resolveCredentials() - .then(({ credentials, identityId }) => { + .then(async ({ credentials, identityId }) => { const timestamp = Date.now(); const { sessionId: cachedSessionId, userId: cachedUserId } = - resolveCachedSession(trackingId); + await resolveCachedSession(); if (eventType === IDENTIFY_EVENT_TYPE) { updateCachedSession( typeof properties.userId === 'string' ? properties.userId : '', @@ -54,7 +52,7 @@ export const record = ({ } const { sessionId: updatedSessionId, userId: updatedUserId } = - resolveCachedSession(trackingId); + await resolveCachedSession(); const eventBuffer = getEventBuffer({ region, diff --git a/packages/analytics/src/providers/personalize/types/buffer.ts b/packages/analytics/src/providers/personalize/types/buffer.ts index 369035873a9..85881d6981c 100644 --- a/packages/analytics/src/providers/personalize/types/buffer.ts +++ b/packages/analytics/src/providers/personalize/types/buffer.ts @@ -3,7 +3,7 @@ import { PersonalizeEvent } from './'; import { EventBufferConfig } from '../../../utils'; -import { Credentials } from '@aws-sdk/types'; +import { AWSCredentials } from '@aws-amplify/core/internals/utils'; export type PersonalizeBufferEvent = { trackingId: string; @@ -15,7 +15,7 @@ export type PersonalizeBufferEvent = { export type PersonalizeBufferConfig = EventBufferConfig & { region: string; - credentials: Credentials; + credentials: AWSCredentials; identityId?: string; userAgentValue?: string; }; diff --git a/packages/analytics/src/providers/personalize/utils/autoTrackMedia.ts b/packages/analytics/src/providers/personalize/utils/autoTrackMedia.ts index dbb30063aec..cda7149af24 100644 --- a/packages/analytics/src/providers/personalize/utils/autoTrackMedia.ts +++ b/packages/analytics/src/providers/personalize/utils/autoTrackMedia.ts @@ -3,7 +3,8 @@ import { EventBuffer } from '../../../utils'; import { PersonalizeBufferEvent, PersonalizeEvent } from '../types'; -import { ConsoleLogger, isBrowser } from '@aws-amplify/core/internals/utils'; +import { isBrowser } from '@aws-amplify/core/internals/utils'; +import { ConsoleLogger } from '@aws-amplify/core'; enum HTML5_MEDIA_EVENT { 'PLAY' = 'play', diff --git a/packages/analytics/src/providers/personalize/utils/cachedSession.ts b/packages/analytics/src/providers/personalize/utils/cachedSession.ts index 8b844f92a3d..e791fc5691f 100644 --- a/packages/analytics/src/providers/personalize/utils/cachedSession.ts +++ b/packages/analytics/src/providers/personalize/utils/cachedSession.ts @@ -2,8 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Cache } from '@aws-amplify/core'; -import { isBrowser } from '@aws-amplify/core/internals/utils'; -import { v4 as uuid } from 'uuid'; +import { isBrowser, amplifyUuid } from '@aws-amplify/core/internals/utils'; const PERSONALIZE_CACHE_USERID = '_awsct_uid'; const PERSONALIZE_CACHE_SESSIONID = '_awsct_sid'; @@ -27,14 +26,16 @@ const setCache = (key: string, value: unknown) => { }); }; -export const resolveCachedSession = (trackingId: string) => { - let sessionId: string | undefined = getCache(PERSONALIZE_CACHE_SESSIONID); +export const resolveCachedSession = async () => { + let sessionId: string | undefined = await getCache( + PERSONALIZE_CACHE_SESSIONID + ); if (!sessionId) { - sessionId = uuid(); + sessionId = amplifyUuid(); setCache(PERSONALIZE_CACHE_SESSIONID, sessionId); } - const userId: string | undefined = getCache(PERSONALIZE_CACHE_USERID); + const userId: string | undefined = await getCache(PERSONALIZE_CACHE_USERID); return { sessionId, @@ -58,7 +59,7 @@ export const updateCachedSession = ( !!currentSessionId && !currentUserId && !!newUserId; if (isRequireNewSession) { - const newSessionId = uuid(); + const newSessionId = amplifyUuid(); setCache(PERSONALIZE_CACHE_SESSIONID, newSessionId); setCache(PERSONALIZE_CACHE_USERID, newUserId); } else if (isRequireUpdateSession) { diff --git a/packages/analytics/src/providers/pinpoint/apis/configureAutoTrack.ts b/packages/analytics/src/providers/pinpoint/apis/configureAutoTrack.ts new file mode 100644 index 00000000000..e50fe6002bd --- /dev/null +++ b/packages/analytics/src/providers/pinpoint/apis/configureAutoTrack.ts @@ -0,0 +1,49 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { AnalyticsValidationErrorCode } from '../../../errors'; +import { + TrackerType, + TrackerAttributes, + TrackerInterface, +} from '../../../types/trackers'; +import { + updateProviderTrackers, + validateTrackerConfiguration, +} from '../../../utils'; +import { ConfigureAutoTrackInput } from '../types'; +import { record } from './record'; + +// Configured Tracker instances for Pinpoint +const configuredTrackers: Partial> = {}; + +// Callback that will emit an appropriate event to Pinpoint when required by the Tracker +const emitTrackingEvent = ( + eventName: string, + attributes: TrackerAttributes +) => { + record({ + name: eventName, + attributes, + }); +}; + +/** + * Configures automatic event tracking for Pinpoint. This API will automatically transmit an analytic event when + * configured events are detected within your application. This can include: DOM element events (via the `event` + * tracker), session events (via the `session` tracker), and page view events (via the `pageView` tracker). + * + * @remark Only session tracking is currently supported on React Native. + * + * @param {ConfigureAutoTrackInput} params The input object to configure auto track behavior. + * + * @throws service: {@link UpdateEndpointException} - Thrown when the underlying Pinpoint service returns an error. + * @throws validation: {@link AnalyticsValidationErrorCode} - Thrown when the provided parameters or library + * configuration is incorrect. + */ +export const configureAutoTrack = (input: ConfigureAutoTrackInput): void => { + validateTrackerConfiguration(input); + + // Initialize or update this provider's trackers + updateProviderTrackers(input, emitTrackingEvent, configuredTrackers); +}; diff --git a/packages/analytics/src/providers/pinpoint/apis/flushEvents.ts b/packages/analytics/src/providers/pinpoint/apis/flushEvents.ts index 4c7ab69b5c7..ac41ee5fe95 100644 --- a/packages/analytics/src/providers/pinpoint/apis/flushEvents.ts +++ b/packages/analytics/src/providers/pinpoint/apis/flushEvents.ts @@ -3,10 +3,8 @@ import { resolveConfig, resolveCredentials } from '../utils'; import { flushEvents as flushEventsCore } from '@aws-amplify/core/internals/providers/pinpoint'; -import { - AnalyticsAction, - ConsoleLogger, -} from '@aws-amplify/core/internals/utils'; +import { AnalyticsAction } from '@aws-amplify/core/internals/utils'; +import { ConsoleLogger } from '@aws-amplify/core'; import { getAnalyticsUserAgentString } from '../../../utils'; const logger = new ConsoleLogger('Analytics'); diff --git a/packages/analytics/src/providers/pinpoint/apis/identifyUser.ts b/packages/analytics/src/providers/pinpoint/apis/identifyUser.ts index 72443820c37..c93a21ccacb 100644 --- a/packages/analytics/src/providers/pinpoint/apis/identifyUser.ts +++ b/packages/analytics/src/providers/pinpoint/apis/identifyUser.ts @@ -2,10 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 import { AnalyticsAction } from '@aws-amplify/core/internals/utils'; -import { updateEndpoint } from '@aws-amplify/core/internals/providers/pinpoint'; +import { + updateEndpoint, + UpdateEndpointException, +} from '@aws-amplify/core/internals/providers/pinpoint'; import { AnalyticsValidationErrorCode } from '../../../errors'; import { getAnalyticsUserAgentString } from '../../../utils'; -import { IdentifyUserInput, UpdateEndpointException } from '../types'; +import { IdentifyUserInput } from '../types'; import { resolveConfig, resolveCredentials } from '../utils'; /** @@ -60,7 +63,7 @@ export const identifyUser = async ({ }: IdentifyUserInput): Promise => { const { credentials, identityId } = await resolveCredentials(); const { appId, region } = resolveConfig(); - const { userAttributes } = options?.serviceOptions ?? {}; + const { userAttributes } = options ?? {}; updateEndpoint({ appId, category: 'Analytics', diff --git a/packages/analytics/src/providers/pinpoint/apis/index.ts b/packages/analytics/src/providers/pinpoint/apis/index.ts index 7c9583f8ca8..6451503dd6a 100644 --- a/packages/analytics/src/providers/pinpoint/apis/index.ts +++ b/packages/analytics/src/providers/pinpoint/apis/index.ts @@ -3,4 +3,5 @@ export { record } from './record'; export { identifyUser } from './identifyUser'; +export { configureAutoTrack } from './configureAutoTrack'; export { flushEvents } from './flushEvents'; diff --git a/packages/analytics/src/providers/pinpoint/apis/record.ts b/packages/analytics/src/providers/pinpoint/apis/record.ts index 3835148bac0..8422d0ffb20 100644 --- a/packages/analytics/src/providers/pinpoint/apis/record.ts +++ b/packages/analytics/src/providers/pinpoint/apis/record.ts @@ -1,11 +1,10 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Hub } from '@aws-amplify/core'; +import { Hub, ConsoleLogger } from '@aws-amplify/core'; import { AMPLIFY_SYMBOL, AnalyticsAction, - ConsoleLogger as Logger, } from '@aws-amplify/core/internals/utils'; import { record as recordCore } from '@aws-amplify/core/internals/providers/pinpoint'; import { @@ -19,7 +18,7 @@ import { import { RecordInput } from '../types'; import { resolveConfig, resolveCredentials } from '../utils'; -const logger = new Logger('Analytics'); +const logger = new ConsoleLogger('Analytics'); /** * Records an Analytic event to Pinpoint. Events will be buffered and periodically sent to Pinpoint. diff --git a/packages/analytics/src/providers/pinpoint/index.ts b/packages/analytics/src/providers/pinpoint/index.ts index e8c93d6fd90..6502ff56f6c 100644 --- a/packages/analytics/src/providers/pinpoint/index.ts +++ b/packages/analytics/src/providers/pinpoint/index.ts @@ -1,5 +1,9 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export { record, identifyUser, flushEvents } from './apis'; -export { RecordInput, IdentifyUserInput } from './types/inputs'; +export { record, identifyUser, flushEvents, configureAutoTrack } from './apis'; +export { + RecordInput, + IdentifyUserInput, + ConfigureAutoTrackInput, +} from './types/inputs'; diff --git a/packages/analytics/src/providers/pinpoint/types/index.ts b/packages/analytics/src/providers/pinpoint/types/index.ts index 75049077495..5eed2fb4477 100644 --- a/packages/analytics/src/providers/pinpoint/types/index.ts +++ b/packages/analytics/src/providers/pinpoint/types/index.ts @@ -1,6 +1,9 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export { UpdateEndpointException } from './errors'; -export { RecordInput, IdentifyUserInput } from './inputs'; +export { + RecordInput, + IdentifyUserInput, + ConfigureAutoTrackInput, +} from './inputs'; export { IdentifyUserOptions } from './options'; diff --git a/packages/analytics/src/providers/pinpoint/types/inputs.ts b/packages/analytics/src/providers/pinpoint/types/inputs.ts index 6a84ed9722c..5dbd0880d50 100644 --- a/packages/analytics/src/providers/pinpoint/types/inputs.ts +++ b/packages/analytics/src/providers/pinpoint/types/inputs.ts @@ -3,7 +3,10 @@ import { PinpointAnalyticsEvent } from '@aws-amplify/core/internals/providers/pinpoint'; import { IdentifyUserOptions } from '.'; -import { AnalyticsIdentifyUserInput } from '../../../types'; +import { + AnalyticsConfigureAutoTrackInput, + AnalyticsIdentifyUserInput, +} from '../../../types'; /** * Input type for Pinpoint record API. @@ -14,3 +17,8 @@ export type RecordInput = PinpointAnalyticsEvent; * Input type for Pinpoint identifyUser API. */ export type IdentifyUserInput = AnalyticsIdentifyUserInput; + +/** + * Input type for Pinpoint configureAutoTrack API. + */ +export type ConfigureAutoTrackInput = AnalyticsConfigureAutoTrackInput; diff --git a/packages/analytics/src/trackers/EventTracker.ts b/packages/analytics/src/trackers/EventTracker.ts new file mode 100644 index 00000000000..68823b68ad2 --- /dev/null +++ b/packages/analytics/src/trackers/EventTracker.ts @@ -0,0 +1,135 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + EventTrackingOptions, + DOMEvent, + TrackerEventRecorder, + TrackerInterface, +} from '../types/trackers'; +import { ConsoleLogger } from '@aws-amplify/core'; +import { isBrowser } from '@aws-amplify/core/internals/utils'; + +const DEFAULT_EVENTS = ['click'] as DOMEvent[]; +const DEFAULT_SELECTOR_PREFIX = 'data-amplify-analytics-'; +const DEFAULT_EVENT_NAME = 'event'; // Default event name as sent to the analytics provider + +const logger = new ConsoleLogger('EventTracker'); + +export class EventTracker implements TrackerInterface { + private trackerActive: boolean; + private options: EventTrackingOptions; + private eventRecorder: TrackerEventRecorder; + + constructor( + eventRecorder: TrackerEventRecorder, + options?: EventTrackingOptions + ) { + this.options = {}; + this.trackerActive = false; + this.eventRecorder = eventRecorder; + this.handleDocEvent = this.handleDocEvent.bind(this); + + this.configure(eventRecorder, options); + } + + public configure( + eventRecorder: TrackerEventRecorder, + options?: EventTrackingOptions + ) { + this.eventRecorder = eventRecorder; + + // Clean up any existing listeners + this.cleanup(); + + // Apply defaults + this.options = { + attributes: options?.attributes ?? undefined, + events: options?.events ?? DEFAULT_EVENTS, + selectorPrefix: options?.selectorPrefix ?? DEFAULT_SELECTOR_PREFIX, + }; + + // Register event listeners + if (isBrowser()) { + this.options.events?.forEach(targetEvent => { + document.addEventListener(targetEvent, this.handleDocEvent, { + capture: true, + }); + }); + + this.trackerActive = true; + } + } + + public cleanup() { + // No-op if document listener is not active + if (!this.trackerActive) { + return; + } + + // Clean up event listeners + this.options.events?.forEach(targetEvent => { + document.removeEventListener(targetEvent, this.handleDocEvent, { + capture: true, + }); + }); + } + + private handleDocEvent(event: Event) { + /** + * Example DOM element: + * + * ``` + *