Overview

Message Queuing and Transmission

Messages are not immediately sent to the server. The API queues messages and sends them when either a certain amount of time has elapsed or a number of messages have accumulated. On platforms where transmission may have performance impact, such as on mobile devices, the transmission of messages can be directly controlled by your program.

Message transmission is not guaranteed and messages may be lost. In cases of forced application shutdown or storage limitations the API may be unable to transmit queued messages or store them for off-line transmission. Messages may also be discarded if they overflow the API’s queue before they can be transmitted.

Off-line Storage

Your application is not required to always have network connectivity. You can configure the API to either always store the messages locally or to store them when contact with the server is not possible. The API will automatically transmit these locally stored messages to the server when connectivity has been restored.

API Usage Pattern

The general usage pattern of the API is:

Adding Analytics

Start up and Shut down

Until the ApplicationStart method is called the API doesn’t send any messages, and after ApplicationStop has been called it won’t send any messages. You can always call any of the API functions, but you have to be between an Application Start and Stop to generate messages.

You will need to choose where you want to put your ApplicationStart and ApplicationStop. While it is typical to have one of each, you can use multiple Starts and/or multiple Stops depending upon the entry point and exit points to your application. Extra calls to Start and Stop don't nest: it’s the first of each one that defines starting and stopping points of your application.

Company and Application IDs

To start the API you are going to need two IDs: the Company ID and the Application ID.

Both company ID and application ID can be any valid GUID. the application ID represents your application across versions and platforms and usually should not be changed.

Application Start

In versions of the API before 2.0, the only thing you needed to call ApplicationStart was an instance of Configuration. However, now you must also create a PAClient instance. This instance is used for all interactions with the API. This was done so that multiple instances of the API could coexist (e.g. library and executable from two different organizations). This PAClient instance also requires an instance of Configuration, and the minimal data that you need to set there is your Company and Application IDs.

C++ Example

#include "PAClient.h"
...
Configuration* configuration = new Configuration();
configuration->SetCompanyID(L"21EC2020-CCCC-1069-A2DD-08002B30309D");
configuration->SetApplicationID(L"21EC2020-AAA1-1069-A2DD-08002B30309D");
PAClient* paClient = new PAClient();
paClient->ApplicationStart(configuration);

C Example

#include "C_PAClient.h"
...
PVOID configuration = CreateConfiguration();
SetCompanyID(configuration, L"21EC2020-CCCC-1069-A2DD-08002B30309D");
SetApplicationID(configuration, L"21EC2020-AAA0-1069-A2DD-08002B30309D");
  PVOID paClient = CreateClient();
ApplicationStart(paClient, configuration, 0, 0);

Of course, there are many other configuration options you can set. Typical options include:

When ApplicationStart is called, The settings from configuration object are copied by the API. Changing the object that you passed in won't change the API's behavior. All that’s left to do is free up the memory:

C++ Example

// We are done with the configuration so let's free it up
delete configuration;

C Example

// We are done with the configuration so let's free it up
DeleteConfiguration(configuration);

A Note on SSL

When SSL is enabled, certificate verification of the endpoint connection is done by default.

On Windows, the API will always use the default system certificate store to validate the endpoint's certificate.

On Linux, the API will search certain well known locations for a valid certificate store to use to validate the endpoint's certificate. If none can be found, it will fall back to checking the locations specified in the environment variables SSL_CERT_DIR and SSL_CERT_FILE. When sending messages to PreEmptive's so-s.info endpoint, even if no certificate store locations can be accessed, or if the locations found do not contain an appropriate certificate, the Linux API will validate the connection using embedded default certificates.

The behavior of the Linux API can be modified by setting the Configuration::Flags member to a combination of values available in the ConfigurationFlags.h file, and by specifying custom certificates using the Configuration::AddSSLCertificate and Configuration::AddSSLCertificateStore methods. Currently, Windows SSL behavior cannot be modified using the API.

Application Stop

Just before your application terminates you should make a call to ApplicationStop. This generates the messages that are used to calculate the duration of the user's interaction with your application and handles any messages queued in memory.

C++ Example

#include "PAClient.h"
...
paClient->ApplicationStop();

C Example

#include "C_PAClient.h"
...
ApplicationStop(paClient, 0, 0);

As you can see from the C Example's signature there are several optional arguments you can pass to the ApplicationStop as well as to most of the other API functions. These are covered in the Sending Custom Data and Information About Your Applicationsections.

Tracking Feature Use

The most common thing do with the API is to track the use of features in your application. You can track the occurrence of a feature using the FeatureTick method or you can use FeatureStart and FeatureStop to track the occurrence and duration.

Adding Feature Ticks

The FeatureTick generates a message that the endpoint tallies up. All you need to provide a name that defines the feature:

C++ Example

void Pascal::ConstructTriangle(int rows)
{
    paClient->FeatureTick(L"ConstructTriangle");
    ...
}

C Example

void ConstructTriangle(int rows)
{
    FeatureTick(paClient, L"ConstructTriangle", 0, 0, 0);
    ...
}

You can place the call to FeatureTick anywhere in the code including inside conditionals. A function can contain any number of FeatureTick calls, as well as FeatureStart and FeatureStop calls, depending upon how it is used. There does not have to be a one-to-one mapping of functions to features.

Adding Feature Starts and Stops

A pair of FeatureStart and FeatureStop calls is used to measure not only the occurrence of a feature use but also its duration. One possible way to use it is to bracket the entire body of the function with the start and stop:

C++ Example

void Pascal::ConstructTriangle(int rows)
{
    paClient->FeatureStart(L"ConstructTriangle");
    ...
    paClient->FeatureStop(L"ConstructTriangle");
}

C Example

void ConstructTriangle(int rows)
{
    FeatureStart(paClient, L"ConstructTriangle", 0, 0, 0);
    ...
    FeatureStop(paClient, L"ConstructTriangle", 0, 0, 0);
}

It is important that you use the exact, same name in the FeatureStart and FeatureStop calls otherwise it will look like two separate features.

The start and stop can also be used across multiple functions:

C++ Example

void Pascal::PrintHeader()
{
    paClient->FeatureStart(L"PrintIt");
    ...
}
void Pascal::PrintRows()
{
    ...
    paClient->FeatureStop(L"PrintIt");
}

C Example

void PrintHeader()
{
    FeatureStart(paClient, L"PrintIt", 0, 0, 0);
    ...
}
void PrintRows()
{
    ...
    FeatureStop(paClient, L"PrintIt", 0, 0, 0);
}

Sending Custom Data

You can send custom data to the server with feature information or any other types of messages. To send over the data you construct an ExtendedKeys structure and add information to it. You can then pass this in as an argument to the message calls:

C++ Example

#include "PAClient.h"
bool Pascal::ConstructTriangle(int rows)
{
    bool result = false;
    ExtendedKeys* keys = new ExtendedKeys();
    keys->Add(L"rows", n);
    paClient->FeatureStart(L"ConstructTriangle", keys);
    ...
    keys->Add(L"result", result ? L"true" : L"false");
    paClient->FeatureStop(L"ConstructTriangle", keys);
    delete keys;
    return result;
}

C Example

#include "C_PAClient.h"
int ConstructTriangle(int rows)
{
    int result = 0;
    PVOID keys = CreateExtendedKeys();
    AddExtendedKeysInt(keys, L"rows", rows);
    FeatureStart(paClient, L"ConstructTriangle", 0, 0, keys);
    ...
    AddExtendedKeysString(keys, L"result", result ? L"true" : L"false");
    FeatureStop(paClient, L"ConstructTriangle", 0, 0, keys);
    DeleteExtendedKeys(keys);
    return result;
}

Values for keys can be either numeric or strings. You can use the same ExtendedKeys structure as many times as you need. If you set a key with the same name a second time its original value is replaced.

ExtendedKeys have some limits. The length of the name for the key is limited to 2000 characters and the value part for strings is limited to 4000 characters. Numeric values have up to 18 digits of precision with 5 digits to the right of the decimal point.

Once you are done with the structure you will need to dispose of it as in the previous example.

Getting System Information

The API can generate two types of system information: a system profile; performance information.

System Profile

The SystemProfile generates information about the operating system, its environment, and the hardware. Generating the profile is just calling the other APIs:

C++ Example

#include "PAClient.h"
Configuration* configuration = new Configuration();
...
paClient->ApplicationStart(configuration);
paClient->SystemProfile();

C Example

#include "C_PAClient.h"
PVOID configuration = CreateConfiguration();
...
ApplicationStart(paClient, configuration, 0, 0);
SystemProfile(paClient, 0, 0, 0);

It's recommended that you only generate one system profile per application run and that is typically done after the ApplicationStart call. The SystemProfile sends back information many different aspects of the machine running your application:

There are two values in the Configuration that affect the data reported in the profile:

Performance Probe

In contrast to the SystemProfile the PerformanceProbe is designed to be used in multiple places and called many times in your application. The PerformanceProbe includes a name so you can track performance information in multiple places in your application:

C++ Example

#include "PAClient.h"
bool Pascal::ConstructTriangle(int rows)
{
    bool result = false;
    ExtendedKeys* keys = new ExtendedKeys();
    keys->Add(L"rows", n);
    paClient->FeatureStart(L"ConstructTriangle", keys);
    paClient->PerformanceProbe(L"ConstructTriangle.Starting", keys);
    ...
    paClient->PerformanceProbe(L"ConstructTriangle.Finished", keys);
    keys->Add(L"result", result ? L"true" : L"false");
    paClient->FeatureStop(L"ConstructTriangle", keys);
    delete keys;
    return result;
}

C Example

#include "C_PAClient.h"
int ConstructTriangle(int rows)
{
    int result = 0;
    PVOID keys = CreateExtendedKeys();
    AddExtendedKeysInt(keys, L"rows", rows);
    FeatureStart(paClient, L"ConstructTriangle", 0, 0, keys);
    PerformanceProbe(paClient, L"ConstructTriangle.Starting", 0, 0, keys);
    ...
    PerformanceProbe(paClient, L"ConstructTriangle.Finished", 0, 0, keys);
    AddExtendedKeysString(keys, L"result", result ? L"true" : L"false");
    FeatureStop(paClient, L"ConstructTriangle", 0, 0, keys);
    DeleteExtendedKeys(keys);
    return result;
}

The PerformanceProbe only reports on two pieces of information: the CPU usage percentage; the amount of memory allocated by the program.

Reporting Exceptions

The API provides a simple way to report exceptional conditions in your application. The exception reports can be used to track exceptions reported by your application or from third party software. The report can also have user added information added to it to aid support staff. And of course you can always add Extended Key information to track application state.

Exception Event Types

Exception events come in three types:

Again, caught is the default event type. In the following example we will show you how to change the event type.

By default, the API also sends along a list of all components loaded by the application reporting the exception. This behavior can be overridden by passing in a value of “false” for the report_appcomponent parameter of the ReportException function.

Reporting Details

To pass all the information about the exception we use an ExceptionInfo instance:

C++ Example

// New one up
ExceptionInfo* pInfo = new ExceptionInfo();
// -- or --
// Create one on the stack
ExceptionInfo info;
// Use it, then free it
delete pInfo;

C Example

// Create one
PVOID pInfo = CreateExceptionInfo();
// Use it, then free it
DeleteExceptionInfo(pInfo);

Before telling the API to report the exception we need to fill in some details. First we need to indicate the event type as previously described – remember that the default event type is "caught". You also need to indicate the exception type and probably a message:

C++ Example

info.SetType(L"Illegal Value");
info.SetMessage(L"x less than zero");

C Example

SetExceptionInfoType(L"Illegal Value");
SetExceptionInfoMessage(L"x less than zero");

To indicate the origin of the exception, create a BinaryInfo object that will accompany the ExceptionInfo. The type and method name of the BinaryInfo will be added to the stack information of the exception report.

In C++ you can also use method chaining and std::exception instances to fill in the details:

C++ Example

info.Caught(err).Type(L"Logic Error");

Adding User Details

Many GUI programs can display a message box indicating the error that has occurred to the user. This is an opportunity to have the user enter some additional information that developers can use to diagnose the problem. The ExceptionInfo provides a way to attach up to 500 characters of text to the exception report that the user can used to describe the circumstances of the event. It also has a way to pass in contact information, such as a registered user name, email address, or full mailing information back to the server.

C++ Example

GetWindowText(hEmail, email, 500);
GetWindowText(hComment, comment, 500);
info.SetUserContact(email);
info.SetUserComment(comment);

C Example

GetWindowText(hEmail, email, 500);
GetWindowText(hComment, comment, 500);
SetExceptionInfoUserContact(pInfo, email);
SetExceptionInfoUserComment(pInfo, comment);

Sending The Report

Now we that we have talked about all the pieces let's look at some examples of actually sending the exception report:

C++ Example

BinaryInfo binary;
binary.SetClassName(L"Pascal");
binary.SetMethodName(L"ConstructTriangle");
try
{
    // complicated code here
}
catch(std::exception& err)
{
    ExceptionInfo info;
    API::ReportException(info.Caught(err), &binary);
}
catch(...)
{
    ExceptionInfo info;
    API::ReportException(info.Uncaught(), &binary);
}

In this example we show how to report both handled and unhandled exceptions in a try/catch.

When working with C you need to set more of the information yourself:

C Example

GetWindowText(hControl, data, 100);
if(GetLastError() != ERROR_SUCCESS)
{
    PVOID pInfo = CreateExceptionInfo();
    SetExceptionInfoMessage(pInfo, L"GetWindowText() failed!");
    ReportException(pInfo, 0, 0, 0, TRUE);
    DeleteExceptionInfo(pInfo);
}

Advanced Configuration

User Opt-In

Your application should allow user to decide if analytics data is gathered or not. The API provides two ways to control the user opting-in to analytics gathering.

The first way to tell the API about the users opt-in is via the Configuration object. You use this way when your application stores their previous selection somewhere:

C++ Example

#include "PAClient.h"
Configuration* configuration = new Configuration();
configuration->SetOptIn(GetSavedOptInState());
...
paClient->ApplicationStart(configuration);

C Example

#include "C_PAClient.h"
PVOID configuration = CreateConfiguration();
SetInitialOptIn(configuration, GetSavedOptInState());
...
ApplicationStart(paClient, configuration, 0, 0);

The opt-in value in the Configuration changes if the API does an application starts up right away. If it is true then the API starts right up. If it is false then the API makes a copy of the Configuration object for later use, but doesn’t start up, since no messages can be sent.

That brings us to the second way the opt-in can be changed and this is the runtime way by talking directly to the API. You would use this when your application would have a dialog that lets the user change their opt-in status after you have done your ApplicationStart:

C++ Example

paClient->SetOptIn(::SendMessage(optIn, BM_GETSTATE, 0, 0) == BST_CHECKED));

C Example

SetOptIn(paClient, SendMessage(optIn, BM_GETSTATE, 0, 0) == BST_CHECKED));

If the opt-in state changes from false to true the API will do an automatic ApplicationStart and analytics will start being gathered.

There are a couple of things to note about the opt-in state. If the ApplicationStart is skipped because of the opt-in state, the API may still send messages that were previously gather and stored offline. Once it is done sending that data it will go back to the stopped state. If you change the opt-in state from true to false the API does not immediately shut down. Instead it stops analytics gathering, by ignoring requests like FeatureTick et. al., and will send any previously created messages. It will also send the ApplicationStop message once that is called.

Information About Your Application

There is much more information you can send to the configured endpoint about your application than just its application ID.

C++ Example

Configuration* configuration = new Configuration();
configuration->SetCompanyID(L"21EC2020-CCCC-1069-A2DD-08002B30309D");
configuration->SetApplicationID(L"21EC2020-AAA1-1069-A2DD-08002B30309D");
paClient->ApplicationStart(configuration);

C Example

PVOID configuration = CreateConfiguration();
SetCompanyID(configuration, L"21EC2020-CCCC-1069-A2DD-08002B30309D");
SetApplicationID(configuration, L"21EC2020-AAA0-1069-A2DD-08002B30309D");
ApplicationStart(paClient, configuration, 0, 0);

There is one more application value that you can use to identify a particular copy of your application.

C++ Example

configuration->SetApplicationInstanceID(GetSerialNumber());

C Example

SetApplicationInstanceID(GetSerialNumber());

Storing Data Offline

By default the API will try to send messages directly to the configured endpoint. If there is no internet connection or there is a problem reaching the server it will save the data locally for later transmission. Where the offline data is stored depends on the platform. There are several potential places where you could see offline data.

On Windows:

On Linux:

The API determines where the offline data is stored depending on what directories it has permission to write to. On Linux, a PREEMPTIVE_ANALYTICS_DIR environment variable can be set to specify the offline storage location. If the specified location is unable to be used, messages will not be saved offline.

You can control this behavior with the FlowController property contained in the Configuration instance passed to ApplicationStart. The settings that control the APIs behavior are:

Both of these are permanent conditions for the duration of the application run. If you set both of these values to false your call to ApplicationStart will return false indicating the API did not start up.

There is also a control called SendDisabled. An initial value for it can be set on the FlowController property contained in the Configuration and then you can change its setting while the application run is active. This differs from Offline in that it is a temporary condition. We will talk more about that in Controlling the Transmission Window.

Controlling Data Transmission

The API doesn’t send messages immediately to the server, instead it bundles them up into groups. The number of message that make up a group is based on a combination of the maximum size of the message queue, how quickly your application is generating messages, and how long you want to keep messages in memory. All of this behavior is determined by the FlowController passed into Configuration. API versions prior to version 2.0 had overlapping settings in the Configuration class and the FlowController class. This led to some unintuitive behavior. Now, all overlapping settings have been moved to the FlowController class.

The Flow Controller

The first items we will look at in the FlowController are those controlling the in-memory queue size. This is where messages are held before being sent to the server or to local storage. There are two controls:

When you set the QueueSize the HighWater is automatically adjusted to be 2/3 of the ~QueueSize`. You may want to adjust these values if your application can send rapid bursts of messages.

The next two items control how often the API will try to flush the queue regardless of how many messages have accumulated.

Adjusting these values requires care. Having a MaximumInterval that is too large can cause the API to hang on to memory used to store messages – this can be exacerbated by having a large QueueSize. Setting it too small can steal time from your application.

The actual time between checks is determined by the FlowController and the rate at which your application is sending messages. This brings us to the next two controls:

Finally, there are two controls that let the FlowController handles transmission problems:

Controlling the Transmission Window

The FlowController uses the previously mentioned values to adjust how often it will empty the in memory queue of messages. Although this takes place on a background thread with a lower priority it still can happen at a time where your application is doing something critical. The API provides a way to control when the transmission can take place.

If you have places in your application where you need to prevent the API from sending messages you can use the SetSendDisabled method:

C++ Example

paClient->SetSendDisabled(true);
// Time critical code here...
paClient->SetSendDisabled(false);

C Example

SetSendDisabled(paClient, 1);
// Time critical code here...
SetSendDisabled(paClient, 0);

Another way to use SetSendDisabled is only allow sending when your application is in an idle state, for example sitting at a menu screen. In this case you would want to start up the API in the disabled state, which you can do with the Configuration instance:

C++ Example

Configuration* configuration = new Configuration();
configuration->SetSendDisabled(true);
paClient->ApplicationStart(configuration);
// When you enter an idle state
paClient->SetSendDisabled(false);
// When you are busy again
paClient->SetSendDisabled(true);

C Example

PVOID configuration = CreateConfiguration();
SetInitialSendDisabled(configuration, 1);
ApplicationStart(paClient, configuration, 0, 0);
// When you enter an idle state
SetSendDisabled(paClient, 0);
// When you are busy gain
SetSendDisabled(paClient, 1);

The last way that you can control the sending of messages is by telling the API that it should flush any queued messages right away. This is done with the SendMessages method. You can call this method if you have sending enabled or disabled: any messages that are in memory are sent.

You can keep your application in the send disabled state permanently and flush the messages when your application becomes idle:

C++ Example

Configuration* configuration = new Configuration();
configuration->SetSendDisabled(true);
configuration->SetQueueSize(300);
paClient->ApplicationStart(configuration);
// When you enter an idle state
paClient->SendMessages();

C Example

PVOID configuration = CreateConfiguration();
SetInitialSendDisabled(configuration, 1);
SetFlowControllerQueueSize(configuration, 300);
ApplicationStart(paClient, configuration, 0, 0);
// When you enter an idle state
SendMessaages(paClient);

If you decide to use this technique then messages are held in memory until you can SendMessages. Because of this the QueueSize is set rather large so that we will not have an overflow and drop messages.

Managing Multiple Sessions

Each ApplicationStart begins a default session which is ended by its matching ApplicationStop. If your application has the concept of the user's session being the entire run of the application you can ignore sessions and just pass in NULL for the session ID. The API will record all the events on the default session automatically.

If you are adding analytics to an application that has multiple users, such as a server, you will probably want to have multiple session IDs.

Session IDs are GUIDs in string form. All of the API calls, except for ApplicationStart and ApplicationStop take a session ID as an argument.

C++ Example

// When a new user is connected create and store their ID
ConnectUser();
paClient->SessionStart(GetUserSessionID());
// Record some features using the current ID
paClient->FeatureTick(L"ConstructTriangle", GetUserSessionID());
// When a user logs out
paClient->SessionStop(GetUserSessionID());
// And clear it form storage
DisconnectUser();

C Example

// When a new user is connected create and store their ID
ConnectUser();
SessionStart(paClient, GetUserSessionID());
// Record some features using the current ID
FeatureTick(paClient, L"ConstructTriangle", GetUserSessionID());
// When a user logs out
SessionStop(paClient, GetUserSessionID());
// And clear it form storage
DisconnectUser();

Your implementation of ConnectUser and DisconnectUser would manage the binding of the user to the session ID which GetUserSessionID returns it for use by the API functions.

API Language Interoperability

Analytics can be added to applications created in multiple languages. The PreEmptive Analytics API is available for several languages, but only one can control the ApplicationStart and ApplicationStop at a time. The API that starts first controls the scope of the application run and has APIs implemented in other language run in a subordinate fashion. This is done by passing in some internal identifiers from the controlling API to the subordinate one:

C++ Example

// If we are the controlling API-
paClient->ApplicationStart(configuration);
StartOtherAPI(paClient->GetMessageGroup(),paClient->GetDefaultSession());
// If we are the subordinate API-
configuration->SetDefaultIDs(otherMessageGroup, otherDefaultSession);
paClient2->ApplicationStart(configuration);

C Example

// If we are the controlling API-
ApplicationStart(paClient, configuration);
StartOtherAPI(GetMessageGroup(), GetDefaultSession());
// If we are the subordinate API-
SetDefaultIDs(configuration, otherMessageGroup, otherDefaultSession);
ApplicationStart(paClient2, configuration, 0, 0);

Advanced Messages

Tamper Messages

PreEmptive Analytics can be used to track suspected tampering of your application. If your tamper checking code detects that the application has been altered you can report that to the server:

C++ Example

if(!ChecksumMatches())
{
    paClient->TamperEvent();
}

C Example

if(!ChecksumMatches())
{
    TamperEvent(paClient, 0, 0, 0);
}

Tamper detection code can be automatically added to your .NET application by Dotfuscator or to your Java application by DashO.

Expiration Messages

PreEmptive Analytics can be used to track life cycle events on applications that are time locked. You can report on subscription or evaluation copies that are nearing or have reached an expiration date. Both require a Shelf Life ID that you can get from PreEmptive Solutions.

C++ Example

TCHAR* shelfLifeID = L"a83fd44b-d5d2-4171-92ce-d878c8f82113";
int daysLeft = GetEvalDaysLeft();
if(daysLeft <= 0)
{
    paClient->ExpiredEvent(shelfLifeID);
    // Display a dialog, exit the application
}
else
{
    if(daysLeft <= 7)
    {
        paClient->ExpirationWarningEvent(shelfLifeID);
        // Display a dialog
    }
}

C Example

TCHAR* shelfLifeID = L"a83fd44b-d5d2-4171-92ce-d878c8f82113";
int daysLeft = GetEvalDaysLeft();
if(daysLeft <= 0)
{
    ExpiredEvent(paClient, shelfLifeID, 0, 0, 0);
    // Display a dialog, exit the application
}
else
{
    if(daysLeft <= 7)
    {
        ExpirationWarningEvent(paClient, shelfLifeID, 0, 0, 0);
        // Display a dialog
    }
}

Expiration code can be automatically added to your .NET application by Dotfuscator or to your Java application by DashO.