github.com/status-im/status-go@v1.1.0/telemetry/client_test.go (about)

     1  package telemetry
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"net/http"
     8  	"net/http/httptest"
     9  	"os"
    10  	"slices"
    11  	"sync"
    12  	"testing"
    13  	"time"
    14  
    15  	"go.uber.org/zap"
    16  	"google.golang.org/protobuf/proto"
    17  
    18  	"github.com/waku-org/go-waku/waku/v2/api/publish"
    19  	v2protocol "github.com/waku-org/go-waku/waku/v2/protocol"
    20  	"github.com/waku-org/go-waku/waku/v2/protocol/pb"
    21  
    22  	"github.com/stretchr/testify/require"
    23  
    24  	"github.com/status-im/status-go/eth-node/types"
    25  	"github.com/status-im/status-go/protocol/transport"
    26  	"github.com/status-im/status-go/protocol/tt"
    27  	v1protocol "github.com/status-im/status-go/protocol/v1"
    28  	"github.com/status-im/status-go/wakuv2"
    29  )
    30  
    31  var (
    32  	testContentTopic = "/waku/1/0x12345679/rfc26"
    33  )
    34  
    35  func createMockServer(t *testing.T, wg *sync.WaitGroup, expectedType TelemetryType, expectedCondition func(received []TelemetryRequest) (shouldSucceed bool, shouldFail bool)) *httptest.Server {
    36  	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    37  		if r.Method != "POST" {
    38  			t.Errorf("Expected 'POST' request, got '%s'", r.Method)
    39  		}
    40  		if r.URL.EscapedPath() != "/record-metrics" {
    41  			t.Errorf("Expected request to '/record-metrics', got '%s'", r.URL.EscapedPath())
    42  		}
    43  
    44  		// Check the request body is as expected
    45  		var received []TelemetryRequest
    46  		err := json.NewDecoder(r.Body).Decode(&received)
    47  		if err != nil {
    48  			t.Fatal(err)
    49  		}
    50  
    51  		if expectedCondition != nil {
    52  			shouldSucceed, shouldFail := expectedCondition(received)
    53  			if shouldFail {
    54  				w.WriteHeader(http.StatusInternalServerError)
    55  				t.Fail()
    56  				return
    57  			}
    58  			if !shouldSucceed {
    59  				w.WriteHeader(http.StatusOK)
    60  				return
    61  			}
    62  		} else {
    63  			if len(received) != 1 {
    64  				t.Errorf("Unexpected data received: %+v", received)
    65  			} else {
    66  				if received[0].TelemetryType != expectedType {
    67  					t.Errorf("Unexpected telemetry type: got %v, want %v", received[0].TelemetryType, expectedType)
    68  				}
    69  			}
    70  		}
    71  		// If the data is as expected, respond with success
    72  		t.Log("Responding with success")
    73  		responseBody := []map[string]interface{}{
    74  			{"status": "created"},
    75  		}
    76  		body, err := json.Marshal(responseBody)
    77  		if err != nil {
    78  			t.Fatalf("Failed to marshal response body: %v", err)
    79  		}
    80  		w.WriteHeader(http.StatusCreated)
    81  		_, err = w.Write(body)
    82  		if err != nil {
    83  			t.Fatalf("Failed to write response body: %v", err)
    84  		}
    85  		wg.Done()
    86  	}))
    87  }
    88  
    89  func createClient(t *testing.T, mockServerURL string) *Client {
    90  	config := zap.NewDevelopmentConfig()
    91  	config.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
    92  	logger, err := config.Build()
    93  	if err != nil {
    94  		t.Fatalf("Failed to create logger: %v", err)
    95  	}
    96  	return NewClient(logger, mockServerURL, "testUID", "testNode", "1.0", WithSendPeriod(100*time.Millisecond), WithPeerID("16Uiu2HAkvWiyFsgRhuJEb9JfjYxEkoHLgnUQmr1N5mKWnYjxYRVm"))
    97  }
    98  
    99  type expectedCondition func(received []TelemetryRequest) (shouldSucceed bool, shouldFail bool)
   100  
   101  func withMockServer(t *testing.T, expectedType TelemetryType, expectedCondition expectedCondition, testFunc func(ctx context.Context, t *testing.T, client *Client, wg *sync.WaitGroup)) {
   102  	var wg sync.WaitGroup
   103  	wg.Add(1) // Expecting one request
   104  
   105  	mockServer := createMockServer(t, &wg, expectedType, expectedCondition)
   106  	defer mockServer.Close()
   107  
   108  	client := createClient(t, mockServer.URL)
   109  
   110  	ctx, cancel := context.WithCancel(context.Background())
   111  	defer cancel()
   112  
   113  	testFunc(ctx, t, client, &wg)
   114  
   115  	// Wait for the request to be received
   116  	wg.Wait()
   117  }
   118  
   119  func TestClient_ProcessReceivedMessages(t *testing.T) {
   120  	withMockServer(t, ReceivedMessagesMetric, nil, func(ctx context.Context, t *testing.T, client *Client, wg *sync.WaitGroup) {
   121  		// Create a telemetry request to send
   122  		data := ReceivedMessages{
   123  			Filter: transport.Filter{
   124  				ChatID:       "testChat",
   125  				PubsubTopic:  "testTopic",
   126  				ContentTopic: types.StringToTopic(testContentTopic),
   127  			},
   128  			SSHMessage: &types.Message{
   129  				Hash:      []byte("hash"),
   130  				Timestamp: uint32(time.Now().Unix()),
   131  			},
   132  			Messages: []*v1protocol.StatusMessage{
   133  				{
   134  					ApplicationLayer: v1protocol.ApplicationLayer{
   135  						ID:   types.HexBytes("123"),
   136  						Type: 1,
   137  					},
   138  				},
   139  			},
   140  		}
   141  
   142  		// Send the telemetry request
   143  		client.Start(ctx)
   144  		client.PushReceivedMessages(ctx, data)
   145  	})
   146  }
   147  
   148  func TestClient_ProcessReceivedEnvelope(t *testing.T) {
   149  	withMockServer(t, ReceivedEnvelopeMetric, nil, func(ctx context.Context, t *testing.T, client *Client, wg *sync.WaitGroup) {
   150  		// Create a telemetry request to send
   151  		envelope := v2protocol.NewEnvelope(&pb.WakuMessage{
   152  			Payload:      []byte{1, 2, 3, 4, 5},
   153  			ContentTopic: testContentTopic,
   154  			Version:      proto.Uint32(0),
   155  			Timestamp:    proto.Int64(time.Now().Unix()),
   156  		}, 0, "")
   157  
   158  		// Send the telemetry request
   159  		client.Start(ctx)
   160  		client.PushReceivedEnvelope(ctx, envelope)
   161  	})
   162  }
   163  
   164  func TestClient_ProcessSentEnvelope(t *testing.T) {
   165  	withMockServer(t, SentEnvelopeMetric, nil, func(ctx context.Context, t *testing.T, client *Client, wg *sync.WaitGroup) {
   166  		// Create a telemetry request to send
   167  		sentEnvelope := wakuv2.SentEnvelope{
   168  			Envelope: v2protocol.NewEnvelope(&pb.WakuMessage{
   169  				Payload:      []byte{1, 2, 3, 4, 5},
   170  				ContentTopic: testContentTopic,
   171  				Version:      proto.Uint32(0),
   172  				Timestamp:    proto.Int64(time.Now().Unix()),
   173  			}, 0, ""),
   174  			PublishMethod: publish.LightPush,
   175  		}
   176  
   177  		// Send the telemetry request
   178  		client.Start(ctx)
   179  		client.PushSentEnvelope(ctx, sentEnvelope)
   180  	})
   181  }
   182  
   183  var (
   184  	testENRBootstrap = "enrtree://AI4W5N5IFEUIHF5LESUAOSMV6TKWF2MB6GU2YK7PU4TYUGUNOCEPW@store.staging.status.nodes.status.im"
   185  )
   186  
   187  func TestTelemetryUponPublishError(t *testing.T) {
   188  	withMockServer(t, ErrorSendingEnvelopeMetric, nil, func(ctx context.Context, t *testing.T, client *Client, wg *sync.WaitGroup) {
   189  		enrTreeAddress := testENRBootstrap
   190  		envEnrTreeAddress := os.Getenv("ENRTREE_ADDRESS")
   191  		if envEnrTreeAddress != "" {
   192  			enrTreeAddress = envEnrTreeAddress
   193  		}
   194  
   195  		wakuConfig := &wakuv2.Config{}
   196  		wakuConfig.Port = 0
   197  		wakuConfig.EnablePeerExchangeClient = true
   198  		wakuConfig.LightClient = true
   199  		wakuConfig.EnableDiscV5 = false
   200  		wakuConfig.DiscV5BootstrapNodes = []string{enrTreeAddress}
   201  		wakuConfig.DiscoveryLimit = 20
   202  		wakuConfig.ClusterID = 16
   203  		wakuConfig.WakuNodes = []string{enrTreeAddress}
   204  		wakuConfig.TelemetryServerURL = client.serverURL
   205  		wakuConfig.TelemetrySendPeriodMs = 500
   206  		w, err := wakuv2.New(nil, "", wakuConfig, nil, nil, nil, nil, nil)
   207  		require.NoError(t, err)
   208  
   209  		client.Start(ctx)
   210  		w.SetStatusTelemetryClient(client)
   211  
   212  		// Setting this forces the publish function to fail when sending a message
   213  		w.SkipPublishToTopic(true)
   214  
   215  		err = w.Start()
   216  		require.NoError(t, err)
   217  
   218  		msg := &pb.WakuMessage{
   219  			Payload:      []byte{1, 2, 3, 4, 5},
   220  			ContentTopic: testContentTopic,
   221  			Version:      proto.Uint32(0),
   222  			Timestamp:    proto.Int64(time.Now().Unix()),
   223  		}
   224  
   225  		// This should result in a single request sent by the telemetry client
   226  		_, err = w.Send(wakuConfig.DefaultShardPubsubTopic, msg, nil)
   227  		require.NoError(t, err)
   228  	})
   229  }
   230  
   231  func TestRetryCache(t *testing.T) {
   232  	counter := 0
   233  	var wg sync.WaitGroup
   234  	wg.Add(2)
   235  
   236  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   237  		if r.Method != "POST" {
   238  			t.Errorf("Expected 'POST' request, got '%s'", r.Method)
   239  		}
   240  		if r.URL.EscapedPath() != "/record-metrics" {
   241  			t.Errorf("Expected request to '/record-metrics', got '%s'", r.URL.EscapedPath())
   242  		}
   243  
   244  		// Check the request body is as expected
   245  		var received []TelemetryRequest
   246  		err := json.NewDecoder(r.Body).Decode(&received)
   247  		if err != nil {
   248  			t.Fatal(err)
   249  		}
   250  
   251  		// Fail for the first request to make telemetry cache grow
   252  		if counter < 1 {
   253  			counter++
   254  			w.WriteHeader(http.StatusInternalServerError)
   255  			wg.Done()
   256  		} else {
   257  			t.Log("Counter reached, responding with success")
   258  			if len(received) == 4 {
   259  				w.WriteHeader(http.StatusCreated)
   260  				responseBody := []map[string]interface{}{
   261  					{"status": "created"},
   262  				}
   263  				body, err := json.Marshal(responseBody)
   264  				if err != nil {
   265  					t.Fatalf("Failed to marshal response body: %v", err)
   266  				}
   267  				w.WriteHeader(http.StatusCreated)
   268  				_, err = w.Write(body)
   269  				if err != nil {
   270  					t.Fatalf("Failed to write response body: %v", err)
   271  				}
   272  				wg.Done()
   273  			} else {
   274  				t.Fatalf("Expected 4 metrics, got %d", len(received)-1)
   275  			}
   276  		}
   277  	}))
   278  	defer mockServer.Close()
   279  
   280  	ctx := context.Background()
   281  
   282  	client := createClient(t, mockServer.URL)
   283  	client.Start(ctx)
   284  
   285  	for i := 0; i < 3; i++ {
   286  		client.PushReceivedEnvelope(ctx, v2protocol.NewEnvelope(&pb.WakuMessage{
   287  			Payload:      []byte{1, 2, 3, 4, 5},
   288  			ContentTopic: testContentTopic,
   289  			Version:      proto.Uint32(0),
   290  			Timestamp:    proto.Int64(time.Now().Unix()),
   291  		}, 0, ""))
   292  	}
   293  
   294  	time.Sleep(110 * time.Millisecond)
   295  
   296  	require.Equal(t, 3, len(client.telemetryRetryCache))
   297  
   298  	client.PushReceivedEnvelope(ctx, v2protocol.NewEnvelope(&pb.WakuMessage{
   299  		Payload:      []byte{1, 2, 3, 4, 5},
   300  		ContentTopic: testContentTopic,
   301  		Version:      proto.Uint32(0),
   302  		Timestamp:    proto.Int64(time.Now().Unix()),
   303  	}, 0, ""))
   304  
   305  	wg.Wait()
   306  
   307  	time.Sleep(100 * time.Millisecond)
   308  
   309  	require.Equal(t, 0, len(client.telemetryRetryCache))
   310  }
   311  
   312  func TestRetryCacheCleanup(t *testing.T) {
   313  	ctx := context.Background()
   314  
   315  	client := createClient(t, "")
   316  	client.Start(ctx)
   317  
   318  	for i := 0; i < 6000; i++ {
   319  		client.PushReceivedEnvelope(ctx, v2protocol.NewEnvelope(&pb.WakuMessage{
   320  			Payload:      []byte{1, 2, 3, 4, 5},
   321  			ContentTopic: testContentTopic,
   322  			Version:      proto.Uint32(0),
   323  			Timestamp:    proto.Int64(time.Now().Unix()),
   324  		}, 0, ""))
   325  	}
   326  
   327  	time.Sleep(110 * time.Millisecond)
   328  
   329  	require.Equal(t, 6000, len(client.telemetryRetryCache))
   330  
   331  	client.PushReceivedEnvelope(ctx, v2protocol.NewEnvelope(&pb.WakuMessage{
   332  		Payload:      []byte{1, 2, 3, 4, 5},
   333  		ContentTopic: testContentTopic,
   334  		Version:      proto.Uint32(0),
   335  		Timestamp:    proto.Int64(time.Now().Unix()),
   336  	}, 0, ""))
   337  
   338  	time.Sleep(210 * time.Millisecond)
   339  
   340  	require.Equal(t, 5001, len(client.telemetryRetryCache))
   341  }
   342  func setDefaultConfig(config *wakuv2.Config, lightMode bool) {
   343  	config.ClusterID = 16
   344  
   345  	if lightMode {
   346  		config.EnablePeerExchangeClient = true
   347  		config.LightClient = true
   348  		config.EnableDiscV5 = false
   349  	} else {
   350  		config.EnableDiscV5 = true
   351  		config.EnablePeerExchangeServer = true
   352  		config.LightClient = false
   353  		config.EnablePeerExchangeClient = false
   354  	}
   355  }
   356  
   357  var testStoreENRBootstrap = "enrtree://AI4W5N5IFEUIHF5LESUAOSMV6TKWF2MB6GU2YK7PU4TYUGUNOCEPW@store.staging.shards.nodes.status.im"
   358  
   359  func TestPeerCount(t *testing.T) {
   360  	// t.Skip("flaky test")
   361  
   362  	expectedCondition := func(received []TelemetryRequest) (shouldSucceed bool, shouldFail bool) {
   363  		found := slices.ContainsFunc(received, func(req TelemetryRequest) bool {
   364  			t.Log(req)
   365  			return req.TelemetryType == PeerCountMetric
   366  		})
   367  		return found, false
   368  	}
   369  	withMockServer(t, PeerCountMetric, expectedCondition, func(ctx context.Context, t *testing.T, client *Client, wg *sync.WaitGroup) {
   370  		config := &wakuv2.Config{}
   371  		setDefaultConfig(config, false)
   372  		config.DiscV5BootstrapNodes = []string{testStoreENRBootstrap}
   373  		config.DiscoveryLimit = 20
   374  		config.TelemetryServerURL = client.serverURL
   375  		config.TelemetrySendPeriodMs = 1500
   376  		config.TelemetryPeerCountSendPeriod = 1500
   377  		w, err := wakuv2.New(nil, "shards.staging", config, nil, nil, nil, nil, nil)
   378  		require.NoError(t, err)
   379  
   380  		w.SetStatusTelemetryClient(client)
   381  		client.Start(ctx)
   382  
   383  		require.NoError(t, w.Start())
   384  
   385  		err = tt.RetryWithBackOff(func() error {
   386  			if len(w.Peers()) == 0 {
   387  				return errors.New("no peers discovered")
   388  			}
   389  			return nil
   390  		})
   391  
   392  		require.NoError(t, err)
   393  
   394  		require.NotEqual(t, 0, len(w.Peers()))
   395  	})
   396  }
   397  
   398  func TestPeerId(t *testing.T) {
   399  	expectedCondition := func(received []TelemetryRequest) (shouldSucceed bool, shouldFail bool) {
   400  		var data map[string]interface{}
   401  
   402  		err := json.Unmarshal(*received[0].TelemetryData, &data)
   403  		if err != nil {
   404  			return false, true
   405  		}
   406  
   407  		_, ok := data["peerId"]
   408  		require.True(t, ok)
   409  		return ok, false
   410  	}
   411  	withMockServer(t, ReceivedEnvelopeMetric, expectedCondition, func(ctx context.Context, t *testing.T, client *Client, wg *sync.WaitGroup) {
   412  		client.Start(ctx)
   413  
   414  		client.PushReceivedEnvelope(ctx, v2protocol.NewEnvelope(&pb.WakuMessage{
   415  			Payload:      []byte{1, 2, 3, 4, 5},
   416  			ContentTopic: testContentTopic,
   417  			Version:      proto.Uint32(0),
   418  			Timestamp:    proto.Int64(time.Now().Unix()),
   419  		}, 0, ""))
   420  
   421  	})
   422  
   423  }
   424  
   425  func TestPeerCountByShard(t *testing.T) {
   426  	expectedCondition := func(received []TelemetryRequest) (shouldSucceed bool, shouldFail bool) {
   427  		found := slices.ContainsFunc(received, func(req TelemetryRequest) bool {
   428  			return req.TelemetryType == PeerCountByShardMetric
   429  		})
   430  		return found, false
   431  	}
   432  	withMockServer(t, PeerCountByShardMetric, expectedCondition, func(ctx context.Context, t *testing.T, client *Client, wg *sync.WaitGroup) {
   433  		config := &wakuv2.Config{}
   434  		setDefaultConfig(config, false)
   435  		config.DiscV5BootstrapNodes = []string{testStoreENRBootstrap}
   436  		config.DiscoveryLimit = 20
   437  		config.TelemetryServerURL = client.serverURL
   438  		config.TelemetryPeerCountSendPeriod = 1500
   439  		config.TelemetrySendPeriodMs = 1500
   440  		w, err := wakuv2.New(nil, "shards.staging", config, nil, nil, nil, nil, nil)
   441  		require.NoError(t, err)
   442  
   443  		w.SetStatusTelemetryClient(client)
   444  		client.Start(ctx)
   445  
   446  		require.NoError(t, w.Start())
   447  
   448  		err = tt.RetryWithBackOff(func() error {
   449  			if len(w.Peers()) == 0 {
   450  				return errors.New("no peers discovered")
   451  			}
   452  			return nil
   453  		})
   454  
   455  		require.NoError(t, err)
   456  
   457  		require.NotEqual(t, 0, len(w.Peers()))
   458  	})
   459  }
   460  
   461  func TestPeerCountByOrigin(t *testing.T) {
   462  	expectedCondition := func(received []TelemetryRequest) (shouldSucceed bool, shouldFail bool) {
   463  		found := slices.ContainsFunc(received, func(req TelemetryRequest) bool {
   464  			return req.TelemetryType == PeerCountByOriginMetric
   465  		})
   466  		return found, false
   467  	}
   468  	withMockServer(t, PeerCountByOriginMetric, expectedCondition, func(ctx context.Context, t *testing.T, client *Client, wg *sync.WaitGroup) {
   469  		config := &wakuv2.Config{}
   470  		setDefaultConfig(config, false)
   471  		config.DiscV5BootstrapNodes = []string{testStoreENRBootstrap}
   472  		config.DiscoveryLimit = 20
   473  		config.TelemetryServerURL = client.serverURL
   474  		config.TelemetryPeerCountSendPeriod = 1500
   475  		config.TelemetrySendPeriodMs = 1500
   476  		w, err := wakuv2.New(nil, "shards.staging", config, nil, nil, nil, nil, nil)
   477  		require.NoError(t, err)
   478  
   479  		w.SetStatusTelemetryClient(client)
   480  		client.Start(ctx)
   481  
   482  		require.NoError(t, w.Start())
   483  
   484  		err = tt.RetryWithBackOff(func() error {
   485  			if len(w.Peers()) == 0 {
   486  				return errors.New("no peers discovered")
   487  			}
   488  			return nil
   489  		})
   490  
   491  		require.NoError(t, err)
   492  
   493  		require.NotEqual(t, 0, len(w.Peers()))
   494  	})
   495  }