Skip to content

Commit 50193ce

Browse files
authored
add destination metadata plugin (#48)
* add destination metadata plugin * optimizations * add ManuallyEnableDestination * add unit tests * clean up * add to do item * bug fix
1 parent 1725099 commit 50193ce

File tree

8 files changed

+260
-15
lines changed

8 files changed

+260
-15
lines changed

Analytics-CSharp/Segment/Analytics/Plugins.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public abstract class DestinationPlugin : EventPlugin
9292
public override PluginType Type => PluginType.Destination;
9393
public abstract string Key { get; }
9494

95-
private bool _enabled = false;
95+
internal bool _enabled = false;
9696

9797
private readonly Timeline _timeline = new Timeline();
9898

@@ -131,7 +131,7 @@ public void Apply(Action<Plugin> closure)
131131
/// <param name="type">value of <see cref="UpdateType"/>. is the update an initialization or refreshment</param>
132132
public override void Update(Settings settings, UpdateType type)
133133
{
134-
_enabled = settings.Integrations?.ContainsKey(Key) ?? false;
134+
_enabled = Key != null && (settings.Integrations?.ContainsKey(Key) ?? false);
135135
_timeline.Apply(plugin => plugin.Update(settings, type));
136136
}
137137

@@ -246,5 +246,21 @@ public Plugin Add(Plugin plugin)
246246
/// <param name="destinationKey">the key of <see cref="DestinationPlugin"/></param>
247247
/// <returns></returns>
248248
public DestinationPlugin Find(string destinationKey) => Timeline.Find(destinationKey);
249+
250+
/// <summary>
251+
/// Manually enable a destination plugin. This is useful when a given DestinationPlugin doesn't have any Segment tie-ins at all.
252+
/// This will allow the destination to be processed in the same way within this library.
253+
/// </summary>
254+
/// <param name="plugin">Destination plugin that needs to be enabled</param>
255+
public void ManuallyEnableDestination(DestinationPlugin plugin)
256+
{
257+
AnalyticsScope.Launch(AnalyticsDispatcher, async () =>
258+
{
259+
await Store.Dispatch<System.AddDestinationToSettingsAction, System>(
260+
new System.AddDestinationToSettingsAction(plugin.Key));
261+
});
262+
263+
Find(plugin.Key)._enabled = true;
264+
}
249265
}
250266
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using Segment.Serialization;
4+
5+
namespace Segment.Analytics.Plugins
6+
{
7+
public class DestinationMetadataPlugin: Plugin
8+
{
9+
public override PluginType Type => PluginType.Enrichment;
10+
11+
private Settings _settings;
12+
13+
public override void Update(Settings settings, UpdateType type)
14+
{
15+
base.Update(settings, type);
16+
_settings = settings;
17+
}
18+
19+
public override RawEvent Execute(RawEvent incomingEvent)
20+
{
21+
// TODO: Precompute the metadata instead of compute it for every events once we have Sovran changed to synchronize mode
22+
HashSet<string> bundled = new HashSet<string>();
23+
HashSet<string> unbundled = new HashSet<string>();
24+
25+
foreach (Plugin plugin in Analytics.Timeline._plugins[PluginType.Destination]._plugins)
26+
{
27+
if (plugin is DestinationPlugin destinationPlugin && !(plugin is SegmentDestination) && destinationPlugin._enabled)
28+
{
29+
bundled.Add(destinationPlugin.Key);
30+
}
31+
}
32+
33+
// All active integrations, not in `bundled` are put in `unbundled`
34+
foreach (string integration in _settings.Integrations.Keys)
35+
{
36+
if (integration != "Segment.io" && !bundled.Contains(integration))
37+
{
38+
unbundled.Add(integration);
39+
}
40+
}
41+
42+
// All unbundledIntegrations not in `bundled` are put in `unbundled`
43+
JsonArray unbundledIntegrations =
44+
_settings.Integrations?.GetJsonObject("Segment.io")?.GetJsonArray("unbundledIntegrations") ??
45+
new JsonArray();
46+
foreach (JsonElement integration in unbundledIntegrations)
47+
{
48+
string content = ((JsonPrimitive)integration).Content;
49+
if (!bundled.Contains(content))
50+
{
51+
unbundled.Add(content);
52+
}
53+
}
54+
55+
incomingEvent._metadata = new DestinationMetadata
56+
{
57+
Bundled = CreateJsonArray(bundled),
58+
Unbundled = CreateJsonArray(unbundled),
59+
BundledIds = new JsonArray()
60+
};
61+
62+
return incomingEvent;
63+
}
64+
65+
private JsonArray CreateJsonArray(IEnumerable<string> list)
66+
{
67+
var jsonArray = new JsonArray();
68+
foreach (string value in list)
69+
{
70+
jsonArray.Add(value);
71+
}
72+
73+
return jsonArray;
74+
}
75+
}
76+
}

Analytics-CSharp/Segment/Analytics/Plugins/SegmentDestination.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ public override void Configure(Analytics analytics)
6060
{
6161
base.Configure(analytics);
6262

63-
// TODO: Add DestinationMetadata enrichment plugin
63+
// Add DestinationMetadata enrichment plugin
64+
Add(new DestinationMetadataPlugin());
6465

6566
_pipeline = new EventPipeline(
6667
analytics,

Analytics-CSharp/Segment/Analytics/State.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,7 @@ public IState Reduce(IState state)
9393
{
9494
// Check if the settings have this destination
9595
Settings settings = systemState._settings;
96-
JsonObject integrations = settings.Integrations;
97-
integrations[_key] = true;
98-
settings.Integrations = integrations;
96+
settings.Integrations[_key] = true;
9997

10098
result = new System(systemState._configuration, settings, systemState._running);
10199
}

Analytics-CSharp/Segment/Analytics/Timeline.cs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ internal RawEvent Process(RawEvent incomingEvent)
3434
// Enrichment is like middleware, a chance to update the event across the board before going to destinations.
3535
RawEvent enrichmentResult = ApplyPlugins(PluginType.Enrichment, beforeResult);
3636

37-
// Make sure not to update the events during this next cycle. Since each destination may want different
37+
// Make sure not to update the events during this next cycle. Since each destination may want different
3838
// data than other destinations we don't want them conflicting and changing what a real result should be
3939
ApplyPlugins(PluginType.Destination, enrichmentResult);
4040

@@ -155,15 +155,11 @@ internal void Add(Plugin plugin)
155155
{
156156
_plugins.Add(plugin);
157157

158-
Analytics analytics = plugin.Analytics;
159-
analytics.AnalyticsScope.Launch(analytics.AnalyticsDispatcher, async () =>
158+
Settings? settings = plugin.Analytics.Settings();
159+
if (settings.HasValue)
160160
{
161-
Settings? settings = await plugin.Analytics.SettingsAsync();
162-
if (settings.HasValue)
163-
{
164-
plugin.Update(settings.Value, UpdateType.Initial);
165-
}
166-
});
161+
plugin.Update(settings.Value, UpdateType.Initial);
162+
}
167163
}
168164

169165
internal void Remove(Plugin plugin) => _plugins.RemoveAll(tempPlugin => tempPlugin == plugin);

Analytics-CSharp/Segment/Analytics/Types.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33

44
namespace Segment.Analytics
55
{
6+
public class DestinationMetadata
7+
{
8+
public JsonArray Bundled { get; set; }
9+
public JsonArray Unbundled { get; set; }
10+
public JsonArray BundledIds { get; set; }
11+
}
12+
613
public abstract class RawEvent
714
{
815
public virtual string Type { get; set; }
@@ -17,6 +24,8 @@ public abstract class RawEvent
1724

1825
public JsonArray Metrics { get; set; }
1926

27+
public DestinationMetadata _metadata { get; set; }
28+
2029
internal void ApplyRawEventData(RawEvent rawEvent)
2130
{
2231
AnonymousId = rawEvent.AnonymousId;
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
using Moq;
2+
using Segment.Analytics;
3+
using Segment.Analytics.Plugins;
4+
using Segment.Analytics.Utilities;
5+
using Segment.Serialization;
6+
using Tests.Utils;
7+
using Xunit;
8+
9+
namespace Tests.Plugins
10+
{
11+
public class DestinationMetadataPluginTest
12+
{
13+
private readonly Analytics _analytics;
14+
15+
private Settings _settings;
16+
17+
private DestinationMetadataPlugin _metadataPlugin;
18+
19+
public DestinationMetadataPluginTest()
20+
{
21+
_settings = JsonUtility.FromJson<Settings>(
22+
"{\"integrations\":{\"Segment.io\":{\"apiKey\":\"1vNgUqwJeCHmqgI9S1sOm9UHCyfYqbaQ\"}},\"plan\":{},\"edgeFunction\":{}}");
23+
24+
var mockHttpClient = new Mock<HTTPClient>(null, null, null);
25+
mockHttpClient
26+
.Setup(httpClient => httpClient.Settings())
27+
.ReturnsAsync(_settings);
28+
29+
var config = new Configuration(
30+
writeKey: "123",
31+
storageProvider: new DefaultStorageProvider("tests"),
32+
autoAddSegmentDestination: false,
33+
userSynchronizeDispatcher: true,
34+
httpClientProvider: new MockHttpClientProvider(mockHttpClient)
35+
);
36+
_analytics = new Analytics(config);
37+
_metadataPlugin = new DestinationMetadataPlugin();
38+
_metadataPlugin.Configure(_analytics);
39+
_metadataPlugin.Update(_settings, UpdateType.Initial);
40+
}
41+
42+
[Fact]
43+
public void TestBundled()
44+
{
45+
var a = new StubDestinationPlugin("A");
46+
var b = new StubDestinationPlugin("B");
47+
var c = new StubDestinationPlugin("C");
48+
_analytics.Add(a);
49+
_analytics.Add(b);
50+
_analytics.Add(c);
51+
_analytics.ManuallyEnableDestination(a);
52+
_analytics.ManuallyEnableDestination(b);
53+
_analytics.ManuallyEnableDestination(c);
54+
55+
var trackEvent = new TrackEvent("test", new JsonObject());
56+
RawEvent actual = _metadataPlugin.Execute(trackEvent);
57+
58+
Assert.Equal(3, actual._metadata.Bundled.Count);
59+
Assert.Equal(0, actual._metadata.Unbundled.Count);
60+
Assert.Equal(0, actual._metadata.BundledIds.Count);
61+
}
62+
63+
[Fact]
64+
public void TestIntegrationNotInBundled()
65+
{
66+
_settings.Integrations.Add("a", "test");
67+
_settings.Integrations.Add("b", "test");
68+
_settings.Integrations.Add("c", "test");
69+
_metadataPlugin.Update(_settings, UpdateType.Refresh);
70+
71+
var trackEvent = new TrackEvent("test", new JsonObject());
72+
RawEvent actual = _metadataPlugin.Execute(trackEvent);
73+
74+
Assert.Equal(0, actual._metadata.Bundled.Count);
75+
Assert.Equal(3, actual._metadata.Unbundled.Count);
76+
Assert.Equal(0, actual._metadata.BundledIds.Count);
77+
}
78+
79+
[Fact]
80+
public void TestUnbundledIntegrations()
81+
{
82+
_metadataPlugin.Update(new Settings
83+
{
84+
Integrations = new JsonObject
85+
{
86+
["Segment.io"] = new JsonObject
87+
{
88+
["unbundledIntegrations"] = new JsonArray
89+
{
90+
"a", "b", "c"
91+
}
92+
}
93+
}
94+
}, UpdateType.Refresh);
95+
96+
var trackEvent = new TrackEvent("test", new JsonObject());
97+
RawEvent actual = _metadataPlugin.Execute(trackEvent);
98+
99+
Assert.Equal(0, actual._metadata.Bundled.Count);
100+
Assert.Equal(3, actual._metadata.Unbundled.Count);
101+
Assert.Equal(0, actual._metadata.BundledIds.Count);
102+
}
103+
104+
[Fact]
105+
public void TestCombination()
106+
{
107+
// bundled
108+
var a = new StubDestinationPlugin("A");
109+
_analytics.Add(a);
110+
_analytics.ManuallyEnableDestination(a);
111+
112+
113+
_metadataPlugin.Update(new Settings
114+
{
115+
Integrations = new JsonObject
116+
{
117+
// IntegrationNotInBundled
118+
["b"] = "test",
119+
["c"] = "test",
120+
["Segment.io"] = new JsonObject
121+
{
122+
["unbundledIntegrations"] = new JsonArray
123+
{
124+
// UnbundledIntegrations
125+
"d", "e", "f"
126+
}
127+
}
128+
}
129+
}, UpdateType.Refresh);
130+
131+
var trackEvent = new TrackEvent("test", new JsonObject());
132+
RawEvent actual = _metadataPlugin.Execute(trackEvent);
133+
134+
Assert.Equal(1, actual._metadata.Bundled.Count);
135+
Assert.Equal(5, actual._metadata.Unbundled.Count);
136+
Assert.Equal(0, actual._metadata.BundledIds.Count);
137+
}
138+
}
139+
}

Tests/Utils/Stubs.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ public class StubEventPlugin : EventPlugin
4545
public override PluginType Type => PluginType.Before;
4646
}
4747

48+
public class StubDestinationPlugin : DestinationPlugin
49+
{
50+
public override string Key { get; }
51+
52+
public StubDestinationPlugin(string key)
53+
{
54+
Key = key;
55+
}
56+
}
57+
4858
public class MockStorageProvider : IStorageProvider
4959
{
5060
public Mock<IStorage> Mock { get; set; }

0 commit comments

Comments
 (0)