diff --git a/assets/docs/configuration/transformations/snowplow-builtin/spGtmssPreview-full-example .hcl b/assets/docs/configuration/transformations/snowplow-builtin/spGtmssPreview-full-example .hcl deleted file mode 100644 index 63505ac6..00000000 --- a/assets/docs/configuration/transformations/snowplow-builtin/spGtmssPreview-full-example .hcl +++ /dev/null @@ -1,4 +0,0 @@ -transform { - use "spGtmssPreview" { - } -} diff --git a/assets/docs/configuration/transformations/snowplow-builtin/spGtmssPreview-full-example.hcl b/assets/docs/configuration/transformations/snowplow-builtin/spGtmssPreview-full-example.hcl new file mode 100644 index 00000000..a37c74d9 --- /dev/null +++ b/assets/docs/configuration/transformations/snowplow-builtin/spGtmssPreview-full-example.hcl @@ -0,0 +1,6 @@ +transform { + use "spGtmssPreview" { + # Message expiry time in seconds (comparing current time to the message's collector timestamp). If message is expired, it's sent to failure target. + expiry_seconds = 600 + } +} diff --git a/docs/configuration_transformations_docs_test.go b/docs/configuration_transformations_docs_test.go index 973aba67..6bb6af34 100644 --- a/docs/configuration_transformations_docs_test.go +++ b/docs/configuration_transformations_docs_test.go @@ -44,7 +44,7 @@ func TestBuiltinTransformationDocumentation(t *testing.T) { } func TestBuiltinSnowplowTransformationDocumentation(t *testing.T) { - transformationsToTest := []string{"spEnrichedFilter", "spEnrichedFilterContext", "spEnrichedFilterUnstructEvent", "spEnrichedSetPk", "spEnrichedToJson"} + transformationsToTest := []string{"spEnrichedFilter", "spEnrichedFilterContext", "spEnrichedFilterUnstructEvent", "spEnrichedSetPk", "spEnrichedToJson", "spGtmssPreview"} for _, tfm := range transformationsToTest { diff --git a/pkg/transform/snowplow_gtmss_preview.go b/pkg/transform/snowplow_gtmss_preview.go index 57b4db81..9eaf0e9b 100644 --- a/pkg/transform/snowplow_gtmss_preview.go +++ b/pkg/transform/snowplow_gtmss_preview.go @@ -1,7 +1,9 @@ package transform import ( + "encoding/base64" "errors" + "time" "github.com/snowplow/snowbridge/config" "github.com/snowplow/snowbridge/pkg/models" @@ -11,6 +13,7 @@ import ( // GTMSSPreviewConfig is a configuration object for the spEnrichedToJson transformation type GTMSSPreviewConfig struct { + Expiry int `hcl:"expiry_seconds,optional"` } // The gtmssPreviewAdapter implements the Pluggable interface @@ -18,7 +21,8 @@ type gtmssPreviewAdapter func(i interface{}) (interface{}, error) // ProvideDefault implements the ComponentConfigurable interface func (f gtmssPreviewAdapter) ProvideDefault() (interface{}, error) { - return nil, nil + cfg := >MSSPreviewConfig{Expiry: 300} // seconds -> 5 minutes + return cfg, nil } // Create implements the ComponentCreator interface. @@ -27,22 +31,24 @@ func (f gtmssPreviewAdapter) Create(i interface{}) (interface{}, error) { } // gtmssPreviewAdapterGenerator returns a gtmssPreviewAdapter -func gtmssPreviewAdapterGenerator(f func() (TransformationFunction, error)) gtmssPreviewAdapter { +func gtmssPreviewAdapterGenerator(f func(cfg *GTMSSPreviewConfig) (TransformationFunction, error)) gtmssPreviewAdapter { return func(i interface{}) (interface{}, error) { - if i != nil { + cfg, ok := i.(*GTMSSPreviewConfig) + if !ok { return nil, errors.New("unexpected configuration input for gtmssPreview transformation") } - return f() + return f(cfg) } } // gtmssPreviewConfigFunction returns a transformation function -func gtmssPreviewConfigFunction() (TransformationFunction, error) { +func gtmssPreviewConfigFunction(cfg *GTMSSPreviewConfig) (TransformationFunction, error) { ctx := "contexts_com_google_tag-manager_server-side_preview_mode_1" property := "x-gtm-server-preview" header := "x-gtm-server-preview" - return gtmssPreviewTransformation(ctx, property, header), nil + expiry := time.Duration(cfg.Expiry) * time.Second + return gtmssPreviewTransformation(ctx, property, header, expiry), nil } // GTMSSPreviewConfigPair is the configuration pair for the gtmss preview transformation @@ -52,7 +58,7 @@ var GTMSSPreviewConfigPair = config.ConfigurationPair{ } // gtmssPreviewTransformation returns a transformation function -func gtmssPreviewTransformation(ctx, property, headerKey string) TransformationFunction { +func gtmssPreviewTransformation(ctx, property, headerKey string, expiry time.Duration) TransformationFunction { return func(message *models.Message, interState interface{}) (*models.Message, *models.Message, *models.Message, interface{}) { parsedEvent, err := IntermediateAsSpEnrichedParsed(interState, message) if err != nil { @@ -60,6 +66,19 @@ func gtmssPreviewTransformation(ctx, property, headerKey string) TransformationF return nil, nil, message, nil } + tstamp, err := parsedEvent.GetValue("collector_tstamp") + if err != nil { + message.SetError(err) + return nil, nil, message, nil + } + + if collectorTstamp, ok := tstamp.(time.Time); ok { + if time.Now().UTC().After(collectorTstamp.Add(expiry)) { + message.SetError(errors.New("Message has expired")) + return nil, nil, message, nil + } + } + headerVal, err := extractHeaderValue(parsedEvent, ctx, property) if err != nil { message.SetError(err) @@ -96,6 +115,10 @@ func extractHeaderValue(parsedEvent analytics.ParsedEvent, ctx, prop string) (*s return nil, errors.New("invalid header value") } + _, err = base64.StdEncoding.DecodeString(headerVal) + if err != nil { + return nil, err + } return &headerVal, nil } diff --git a/pkg/transform/snowplow_gtmss_preview_test.go b/pkg/transform/snowplow_gtmss_preview_test.go index 76d78bd8..1cc302ec 100644 --- a/pkg/transform/snowplow_gtmss_preview_test.go +++ b/pkg/transform/snowplow_gtmss_preview_test.go @@ -5,6 +5,7 @@ import ( "reflect" "strings" "testing" + "time" "github.com/davecgh/go-spew/spew" "github.com/stretchr/testify/assert" @@ -13,12 +14,15 @@ import ( "github.com/snowplow/snowplow-golang-analytics-sdk/analytics" ) +const fiftyYears = time.Hour * 24 * 365 * 50 + func TestGTMSSPreview(t *testing.T) { testCases := []struct { Scenario string Ctx string Property string HeaderKey string + Expiry time.Duration InputMsg *models.Message InputInterState interface{} Expected map[string]*models.Message @@ -30,6 +34,7 @@ func TestGTMSSPreview(t *testing.T) { Ctx: "contexts_com_google_tag-manager_server-side_preview_mode_1", Property: "x-gtm-server-preview", HeaderKey: "x-gtm-server-preview", + Expiry: fiftyYears, InputMsg: &models.Message{ Data: spTsvWithGtmss, PartitionKey: "pk", @@ -54,6 +59,7 @@ func TestGTMSSPreview(t *testing.T) { Ctx: "contexts_com_google_tag-manager_server-side_preview_mode_1", Property: "x-gtm-server-preview", HeaderKey: "x-gtm-server-preview", + Expiry: fiftyYears, InputMsg: &models.Message{ Data: spTsvNoGtmss, PartitionKey: "pk", @@ -76,6 +82,7 @@ func TestGTMSSPreview(t *testing.T) { Ctx: "contexts_com_google_tag-manager_server-side_preview_mode_1", Property: "x-gtm-server-preview", HeaderKey: "x-gtm-server-preview", + Expiry: fiftyYears, InputMsg: &models.Message{ Data: []byte(`asdf`), PartitionKey: "pk", @@ -98,6 +105,7 @@ func TestGTMSSPreview(t *testing.T) { Ctx: "contexts_com_google_tag-manager_server-side_preview_mode_1", Property: "x-gtm-server-preview", HeaderKey: "x-gtm-server-preview", + Expiry: fiftyYears, InputMsg: &models.Message{ Data: spTsvWithGtmss, PartitionKey: "pk", @@ -126,6 +134,7 @@ func TestGTMSSPreview(t *testing.T) { Ctx: "contexts_com_google_tag-manager_server-side_preview_mode_1", Property: "x-gtm-server-preview", HeaderKey: "x-gtm-server-preview", + Expiry: fiftyYears, InputMsg: &models.Message{ Data: spTsvWithGtmss, PartitionKey: "pk", @@ -155,6 +164,7 @@ func TestGTMSSPreview(t *testing.T) { Ctx: "contexts_com_google_tag-manager_server-side_preview_mode_1", Property: "x-gtm-server-preview", HeaderKey: "x-gtm-server-preview", + Expiry: fiftyYears, InputMsg: &models.Message{ Data: spTsvWithGtmss, PartitionKey: "pk", @@ -180,6 +190,7 @@ func TestGTMSSPreview(t *testing.T) { Ctx: "app_id", Property: "x-gtm-server-preview", HeaderKey: "x-gtm-server-preview", + Expiry: fiftyYears, InputMsg: &models.Message{ Data: spTsvWithGtmss, PartitionKey: "pk", @@ -200,11 +211,34 @@ func TestGTMSSPreview(t *testing.T) { ExpInterState: spTsvWithGtmssParsed, Error: nil, }, + { + Scenario: "expired_message", + Ctx: "contexts_com_google_tag-manager_server-side_preview_mode_1", + Property: "x-gtm-server-preview", + HeaderKey: "x-gtm-server-preview", + Expiry: 1 * time.Hour, + InputMsg: &models.Message{ + Data: spTsvWithGtmss, + PartitionKey: "pk", + }, + InputInterState: nil, + Expected: map[string]*models.Message{ + "success": nil, + "filtered": nil, + "failed": { + Data: []byte(spTsvWithGtmss), + PartitionKey: "pk", + HTTPHeaders: nil, + }, + }, + ExpInterState: nil, + Error: errors.New("Message has expired"), + }, } for _, tt := range testCases { t.Run(tt.Scenario, func(t *testing.T) { - transFunction := gtmssPreviewTransformation(tt.Ctx, tt.Property, tt.HeaderKey) + transFunction := gtmssPreviewTransformation(tt.Ctx, tt.Property, tt.HeaderKey, tt.Expiry) s, f, e, i := transFunction(tt.InputMsg, tt.InputInterState) if !reflect.DeepEqual(i, tt.ExpInterState) { @@ -281,13 +315,21 @@ func TestExtractHeaderValue(t *testing.T) { Error: nil, }, { - Scenario: "invalid_header_value", + Scenario: "invalid_header_value (no string)", Event: fakeSpTsvParsed, Ctx: "contexts_com_snowplowanalytics_snowplow_web_page_1", Prop: "id", Expected: nil, Error: errors.New("invalid header value"), }, + { + Scenario: "invalid_header_value (no base64)", + Event: gtmssInvalidNoB64Parsed, + Ctx: "contexts_com_google_tag-manager_server-side_preview_mode_1", + Prop: "x-gtm-server-preview", + Expected: nil, + Error: errors.New("illegal base64 data at input"), + }, { Scenario: "event_without_contexts", Event: spTsvNoCtxParsed, @@ -343,7 +385,7 @@ func Benchmark_GTMSSPreview_With_Preview_Ctx_no_intermediate(b *testing.B) { prop := "x-gtm-server-preview" header := "x-gtm-server-preview" - transFunction := gtmssPreviewTransformation(ctx, prop, header) + transFunction := gtmssPreviewTransformation(ctx, prop, header, fiftyYears) for n := 0; n < b.N; n++ { transFunction(inputMsg, nil) @@ -362,7 +404,7 @@ func Benchmark_GTMSSPreview_With_Preview_Ctx_With_intermediate(b *testing.B) { prop := "x-gtm-server-preview" header := "x-gtm-server-preview" - transFunction := gtmssPreviewTransformation(ctx, prop, header) + transFunction := gtmssPreviewTransformation(ctx, prop, header, fiftyYears) for n := 0; n < b.N; n++ { transFunction(inputMsg, interState) @@ -380,7 +422,7 @@ func Benchmark_GTMSSPreview_No_Preview_Ctx_no_intermediate(b *testing.B) { prop := "x-gtm-server-preview" header := "x-gtm-server-preview" - transFunction := gtmssPreviewTransformation(ctx, prop, header) + transFunction := gtmssPreviewTransformation(ctx, prop, header, fiftyYears) for n := 0; n < b.N; n++ { transFunction(inputMsg, nil) @@ -399,7 +441,7 @@ func Benchmark_GTMSSPreview_No_Preview_Ctx_With_intermediate(b *testing.B) { prop := "x-gtm-server-preview" header := "x-gtm-server-preview" - transFunction := gtmssPreviewTransformation(ctx, prop, header) + transFunction := gtmssPreviewTransformation(ctx, prop, header, fiftyYears) for n := 0; n < b.N; n++ { transFunction(inputMsg, interState) @@ -449,6 +491,9 @@ var spTsvNoGtmssParsed, _ = analytics.ParseEvent(string(spTsvNoGtmss)) var spTsvWithGtmss = []byte(`media-test web 2024-03-12 04:27:01.760 2024-03-12 04:27:01.755 2024-03-12 04:27:01.743 unstruct 9be3afe8-8a62-41ac-93db-12f425d82ac9 spTest js-3.17.0 snowplow-micro-2.0.0-stdout$ snowplow-micro-2.0.0 media_tester 172.17.0.1 23a0eb65-83f6-4957-839e-f3044bfefb99 1 a2f53212-26a3-4781-81d6-f14aa8d4552b http://localhost:8000/?sgtm-preview-header=ZW52LTcyN3wtMkMwR084ekptbWxiZmpkcHNIRENBfDE4ZTJkYzgxMDc2NDg1MjVmMzI2Mw== http localhost 8000 / sgtm-preview-header=ZW52LTcyN3wtMkMwR084ekptbWxiZmpkcHNIRENBfDE4ZTJkYzgxMDc2NDg1MjVmMzI2Mw== {"schema":"iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0","data":[{"schema":"iglu:org.whatwg/media_element/jsonschema/1-0-0","data":{"htmlId":"bunny-mp4","mediaType":"VIDEO","autoPlay":false,"buffered":[{"start":0,"end":1.291666}],"controls":true,"currentSrc":"https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4","defaultMuted":false,"defaultPlaybackRate":1,"error":null,"networkState":"NETWORK_LOADING","preload":"","readyState":"HAVE_ENOUGH_DATA","seekable":[{"start":0,"end":596.503219}],"seeking":false,"src":"https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4","textTracks":[],"fileExtension":"mp4","fullscreen":false,"pictureInPicture":false}},{"schema":"iglu:com.snowplowanalytics.snowplow/media_player/jsonschema/1-0-0","data":{"currentTime":0,"duration":596.503219,"ended":false,"loop":false,"muted":false,"paused":false,"playbackRate":1,"volume":100}},{"schema":"iglu:org.whatwg/video_element/jsonschema/1-0-0","data":{"poster":"","videoHeight":360,"videoWidth":640}},{"schema":"iglu:com.snowplowanalytics.snowplow/web_page/jsonschema/1-0-0","data":{"id":"021c4d09-e502-4562-8182-5ac7247125ec"}},{"schema":"iglu:com.google.tag-manager.server-side/user_data/jsonschema/1-0-0","data":{"email_address":"foo@example.com","phone_number":"+15551234567","address":{"first_name":"Jane","last_name":"Doe","street":"123 Fake St","city":"San Francisco","region":"CA","postal_code":"94016","country":"US"}}},{"schema":"iglu:com.snowplowanalytics.snowplow/mobile_context/jsonschema/1-0-2","data":{"osType":"testOsType","osVersion":"testOsVersion","deviceManufacturer":"testDevMan","deviceModel":"testDevModel"}},{"schema":"iglu:com.google.tag-manager.server-side/preview_mode/jsonschema/1-0-0","data":{"x-gtm-server-preview":"ZW52LTcyN3wtMkMwR084ekptbWxiZmpkcHNIRENBfDE4ZTJkYzgxMDc2NDg1MjVmMzI2Mw=="}},{"schema":"iglu:com.snowplowanalytics.snowplow/client_session/jsonschema/1-0-2","data":{"userId":"23a0eb65-83f6-4957-839e-f3044bfefb99","sessionId":"73fcdaa3-0164-41ce-a336-fb00c4ebf68c","eventIndex":7,"sessionIndex":1,"previousSessionId":null,"storageMechanism":"COOKIE_1","firstEventId":"327b9ff9-ed5f-40cf-918a-1b1a775ae347","firstEventTimestamp":"2024-03-12T04:25:36.684Z"}}]} {"schema":"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0","data":{"schema":"iglu:com.snowplowanalytics.snowplow/media_player_event/jsonschema/1-0-0","data":{"type":"play"}}} Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0 en-US 1 24 1920 935 Europe/Athens 1920 1080 windows-1252 1920 935 2024-03-12 04:27:01.745 73fcdaa3-0164-41ce-a336-fb00c4ebf68c 2024-03-12 04:27:01.753 com.snowplowanalytics.snowplow media_player_event jsonschema 1-0-0 `) var spTsvWithGtmssParsed, _ = analytics.ParseEvent(string(spTsvWithGtmss)) +var gtmssInvalidNoB64 = []byte(`media-test web 2024-03-12 04:27:01.760 2024-03-12 04:27:01.755 2024-03-12 04:27:01.743 unstruct 9be3afe8-8a62-41ac-93db-12f425d82ac9 spTest js-3.17.0 snowplow-micro-2.0.0-stdout$ snowplow-micro-2.0.0 media_tester 172.17.0.1 23a0eb65-83f6-4957-839e-f3044bfefb99 1 a2f53212-26a3-4781-81d6-f14aa8d4552b http://localhost:8000/?sgtm-preview-header=ZW52LTcyN3wtMkMwR084ekptbWxiZmpkcHNIRENBfDE4ZTJkYzgxMDc2NDg1MjVmMzI2Mw== http localhost 8000 / sgtm-preview-header=ZW52LTcyN3wtMkMwR084ekptbWxiZmpkcHNIRENBfDE4ZTJkYzgxMDc2NDg1MjVmMzI2Mw== {"schema":"iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0","data":[{"schema":"iglu:org.whatwg/media_element/jsonschema/1-0-0","data":{"htmlId":"bunny-mp4","mediaType":"VIDEO","autoPlay":false,"buffered":[{"start":0,"end":1.291666}],"controls":true,"currentSrc":"https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4","defaultMuted":false,"defaultPlaybackRate":1,"error":null,"networkState":"NETWORK_LOADING","preload":"","readyState":"HAVE_ENOUGH_DATA","seekable":[{"start":0,"end":596.503219}],"seeking":false,"src":"https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4","textTracks":[],"fileExtension":"mp4","fullscreen":false,"pictureInPicture":false}},{"schema":"iglu:com.snowplowanalytics.snowplow/media_player/jsonschema/1-0-0","data":{"currentTime":0,"duration":596.503219,"ended":false,"loop":false,"muted":false,"paused":false,"playbackRate":1,"volume":100}},{"schema":"iglu:org.whatwg/video_element/jsonschema/1-0-0","data":{"poster":"","videoHeight":360,"videoWidth":640}},{"schema":"iglu:com.snowplowanalytics.snowplow/web_page/jsonschema/1-0-0","data":{"id":"021c4d09-e502-4562-8182-5ac7247125ec"}},{"schema":"iglu:com.google.tag-manager.server-side/user_data/jsonschema/1-0-0","data":{"email_address":"foo@example.com","phone_number":"+15551234567","address":{"first_name":"Jane","last_name":"Doe","street":"123 Fake St","city":"San Francisco","region":"CA","postal_code":"94016","country":"US"}}},{"schema":"iglu:com.snowplowanalytics.snowplow/mobile_context/jsonschema/1-0-2","data":{"osType":"testOsType","osVersion":"testOsVersion","deviceManufacturer":"testDevMan","deviceModel":"testDevModel"}},{"schema":"iglu:com.google.tag-manager.server-side/preview_mode/jsonschema/1-0-0","data":{"x-gtm-server-preview":"this is not valid base64"}},{"schema":"iglu:com.snowplowanalytics.snowplow/client_session/jsonschema/1-0-2","data":{"userId":"23a0eb65-83f6-4957-839e-f3044bfefb99","sessionId":"73fcdaa3-0164-41ce-a336-fb00c4ebf68c","eventIndex":7,"sessionIndex":1,"previousSessionId":null,"storageMechanism":"COOKIE_1","firstEventId":"327b9ff9-ed5f-40cf-918a-1b1a775ae347","firstEventTimestamp":"2024-03-12T04:25:36.684Z"}}]} {"schema":"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0","data":{"schema":"iglu:com.snowplowanalytics.snowplow/media_player_event/jsonschema/1-0-0","data":{"type":"play"}}} Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0 en-US 1 24 1920 935 Europe/Athens 1920 1080 windows-1252 1920 935 2024-03-12 04:27:01.745 73fcdaa3-0164-41ce-a336-fb00c4ebf68c 2024-03-12 04:27:01.753 com.snowplowanalytics.snowplow media_player_event jsonschema 1-0-0 `) +var gtmssInvalidNoB64Parsed, _ = analytics.ParseEvent(string(gtmssInvalidNoB64)) + var fakeSpTsv = []byte(`media-test web 2024-03-12 04:25:40.277 2024-03-12 04:25:40.272 2024-03-12 04:25:36.685 page_view 1313411b-282f-4aa9-b37c-c60d4723cf47 spTest js-3.17.0 snowplow-micro-2.0.0-stdout$ snowplow-micro-2.0.0 media_tester 172.17.0.1 23a0eb65-83f6-4957-839e-f3044bfefb99 1 a2f53212-26a3-4781-81d6-f14aa8d4552b http://localhost:8000/ Test Media Tracking http localhost 8000 / {"schema":"iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0","data":[{"schema":"iglu:com.snowplowanalytics.snowplow/web_page/jsonschema/1-0-0","data":{"id":["FAILS"]}},{"schema":"iglu:com.google.tag-manager.server-side/user_data/jsonschema/1-0-0","data":{"email_address":"foo@example.com","phone_number":"+15551234567","address":{"first_name":"Jane","last_name":"Doe","street":"123 Fake St","city":"San Francisco","region":"CA","postal_code":"94016","country":"US"}}},{"schema":"iglu:com.snowplowanalytics.snowplow/mobile_context/jsonschema/1-0-2","data":{"osType":"testOsType","osVersion":"testOsVersion","deviceManufacturer":"testDevMan","deviceModel":"testDevModel"}},{"schema":"iglu:com.snowplowanalytics.snowplow/client_session/jsonschema/1-0-2","data":{"userId":"23a0eb65-83f6-4957-839e-f3044bfefb99","sessionId":"73fcdaa3-0164-41ce-a336-fb00c4ebf68c","eventIndex":2,"sessionIndex":1,"previousSessionId":null,"storageMechanism":"COOKIE_1","firstEventId":"327b9ff9-ed5f-40cf-918a-1b1a775ae347","firstEventTimestamp":"2024-03-12T04:25:36.684Z"}}]} Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0 en-US 1 24 1920 935 Europe/Athens 1920 1080 windows-1252 1920 935 2024-03-12 04:25:40.268 73fcdaa3-0164-41ce-a336-fb00c4ebf68c 2024-03-12 04:25:36.689 com.snowplowanalytics.snowplow page_view jsonschema 1-0-0 `) var fakeSpTsvParsed, _ = analytics.ParseEvent(string(fakeSpTsv))