diff --git a/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/configurations/NetworkConfigurationReader.kt b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/configurations/NetworkConfigurationReader.kt index 33a5c94..54087f2 100644 --- a/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/configurations/NetworkConfigurationReader.kt +++ b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/configurations/NetworkConfigurationReader.kt @@ -20,6 +20,7 @@ class NetworkConfigurationReader(values: Map) { val endpoint: String by values val method: String? by valuesDefault val customPostPath: String? by valuesDefault + val requestHeaders: Map? by valuesDefault fun toConfiguration(): NetworkConfiguration { val networkConfig: NetworkConfiguration = if (method != null) { @@ -31,6 +32,7 @@ class NetworkConfigurationReader(values: Map) { NetworkConfiguration(endpoint) } customPostPath?.let { networkConfig.customPostPath(it) } + requestHeaders?.let { networkConfig.requestHeaders(it) } return networkConfig } } diff --git a/doc/02-configuration.md b/doc/02-configuration.md index 78a59f6..f6d8bee 100644 --- a/doc/02-configuration.md +++ b/doc/02-configuration.md @@ -22,6 +22,7 @@ The only required attributes of the `Snowplow.createTracker` method are `namespa | `endpoint` | `String` | URI for the Snowplow collector endpoint. | | `method` | `Method?` | HTTP method to use: `Method.get` or `Method.post` (`Method.post` is default). | | `customPostPath` | `String?` | Custom POST path. | +| `requestHeaders` | `Map?` | Map of custom HTTP headers to add to requests to the collector. | | `trackerConfig` | `TrackerConfiguration?` | Configuration of the tracker and the core tracker properties. | | `gdprConfig` | `GdprConfiguration?` | Determines the GDPR context that will be attached to all events sent by the tracker. | | `subjectConfig` | `SubjectConfiguration?` | Subject information about tracked user and device that is added to events. | diff --git a/example/integration_test/configuration_test.dart b/example/integration_test/configuration_test.dart index c019a56..6dd158c 100644 --- a/example/integration_test/configuration_test.dart +++ b/example/integration_test/configuration_test.dart @@ -246,4 +246,21 @@ void main() { return true; }); }); + + testWidgets("sets custom request headers", (WidgetTester tester) async { + SnowplowTracker tracker = await Snowplow.createTracker( + namespace: 'custom-headers', + endpoint: SnowplowTests.microEndpoint, + requestHeaders: {'Warning': 'works'}); + + await tracker + .track(const Structured(category: 'category', action: 'action')); + + expect( + await SnowplowTests.checkMicroGood((events) => + (events.length == 1) && + (events[0]['rawEvent']['context']['headers'] + .contains('Warning: works'))), + isTrue); + }); } diff --git a/ios/Classes/readers/configurations/NetworkConfigurationReader.swift b/ios/Classes/readers/configurations/NetworkConfigurationReader.swift index 40e9795..d7d6da5 100644 --- a/ios/Classes/readers/configurations/NetworkConfigurationReader.swift +++ b/ios/Classes/readers/configurations/NetworkConfigurationReader.swift @@ -16,6 +16,7 @@ struct NetworkConfigurationReader: Decodable { let endpoint: String let method: String? let customPostPath: String? + let requestHeaders: [String: String]? } extension NetworkConfigurationReader { @@ -31,6 +32,9 @@ extension NetworkConfigurationReader { if let c = customPostPath { networkConfig.customPostPath(c) } + if let h = requestHeaders { + networkConfig.requestHeaders(h) + } return networkConfig } } diff --git a/lib/configurations/network_configuration.dart b/lib/configurations/network_configuration.dart index c5e8de5..e93e536 100644 --- a/lib/configurations/network_configuration.dart +++ b/lib/configurations/network_configuration.dart @@ -26,8 +26,14 @@ class NetworkConfiguration { /// You will need to configure your collector to accept your custom path. final String? customPostPath; + /// Custom HTTP headers to add to HTTP requests to the collector. + final Map? requestHeaders; + const NetworkConfiguration( - {required this.endpoint, this.method, this.customPostPath}); + {required this.endpoint, + this.method, + this.customPostPath, + this.requestHeaders}); Map toMap() { final conf = { @@ -35,7 +41,8 @@ class NetworkConfiguration { 'method': method?.name, 'customPostPath': customPostPath?[0] == '/' ? customPostPath?.substring(1) - : customPostPath + : customPostPath, + 'requestHeaders': requestHeaders }; conf.removeWhere((key, value) => value == null); return conf; diff --git a/lib/snowplow.dart b/lib/snowplow.dart index 6cf6bf0..711ca02 100644 --- a/lib/snowplow.dart +++ b/lib/snowplow.dart @@ -34,11 +34,13 @@ class Snowplow { /// [endpoint] refers to the Snowplow collector endpoint. /// [method] is the HTTP method used to send events to collector and it defaults to POST. /// [customPostPath] is an optional string for custom POST collector paths. + /// [requestHeaders] is an optional map of custom HTTP headers to add to requests to the collector. static Future createTracker( {required String namespace, required String endpoint, Method? method, String? customPostPath, + Map? requestHeaders, TrackerConfiguration? trackerConfig, SubjectConfiguration? subjectConfig, GdprConfiguration? gdprConfig, @@ -46,7 +48,10 @@ class Snowplow { final configuration = Configuration( namespace: namespace, networkConfig: NetworkConfiguration( - endpoint: endpoint, method: method, customPostPath: customPostPath), + endpoint: endpoint, + method: method, + customPostPath: customPostPath, + requestHeaders: requestHeaders), trackerConfig: trackerConfig, subjectConfig: subjectConfig, gdprConfig: gdprConfig, diff --git a/lib/src/web/readers/configurations/network_configuration_reader.dart b/lib/src/web/readers/configurations/network_configuration_reader.dart index 7fc51cd..a14b609 100644 --- a/lib/src/web/readers/configurations/network_configuration_reader.dart +++ b/lib/src/web/readers/configurations/network_configuration_reader.dart @@ -18,7 +18,10 @@ class NetworkConfigurationReader extends NetworkConfiguration { method: map['method'] == null ? null : Method.values.byName(map['method']), - customPostPath: map['customPostPath']); + customPostPath: map['customPostPath'], + requestHeaders: map['requestHeaders'] == null + ? null + : (map['requestHeaders'] as Map).cast()); void addTrackerOptions(dynamic options) { if (method != null) { @@ -27,5 +30,8 @@ class NetworkConfigurationReader extends NetworkConfiguration { if (customPostPath != null) { options['postPath'] = '/$customPostPath'; } + if (requestHeaders != null) { + options['customHeaders'] = requestHeaders; + } } } diff --git a/test/snowplow_test.dart b/test/snowplow_test.dart index 82d72fa..b427139 100644 --- a/test/snowplow_test.dart +++ b/test/snowplow_test.dart @@ -122,6 +122,23 @@ void main() { })); }); + test('createsTrackerWithCustomRequestHeaders', () async { + await Snowplow.createTracker( + namespace: 'tns1', + endpoint: 'https://snowplowanalytics.com', + requestHeaders: {'header1': 'value1', 'header2': 'value2'}); + + expect( + methodCall, + isMethodCall('createTracker', arguments: { + 'namespace': 'tns1', + 'networkConfig': { + 'endpoint': 'https://snowplowanalytics.com', + 'requestHeaders': {'header1': 'value1', 'header2': 'value2'} + } + })); + }); + test('createsTrackerWithEmitterConfig', () async { await Snowplow.createTracker( namespace: 'tns1',