Managing OneSignal Tags

4 minute read

I use OneSignal to send push notifications to my iOS app HKWarnings. It’s great, fast and has a very active open source community on GitHub, even I’ve contributed.

Anyway, it’s been free for about 5(?) years, but every startup eventually needs to make money, so they recently announced paid plans with these limits:

  • Up to 6 Segments: OneSignal’s Free tier will support up to 6 Segments for users with more than 30,000 subscribers.
  • OneSignal’s Free tier will support up to 3 Automated Messages for users with more than 30,000 subscribers.
  • OneSignal’s Free tier will support up to 10 Data Tags per device. This limitation applies to all Free users, regardless of subscriber count.

It’s the last limit that affects me and HKWarnings. I use tags to exclude certain devices when sending certain push notifications1. I also tagged other info, so I quite easily had more than 10 tags.

Hmmm, more than 35,000 subscribers, with all sorts of tags I don’t need, but a few I need.

Let’s check how much it would cost:

onesig_pricing

Hmmm, $114 a month. A bit too much for my free app.

So I needed to remove tags. There are a couple of ways.

From within the app

Using deleteTags from the iOS SDK:

[OneSignal deleteTags:@[@"key1", @"key2"]];

However, this would rely on everyone upgrading and running the app.

Via the REST API

With a PUT call to the device endpoint:

curl --include \
     --request PUT \
     --header "Content-Type: application/json" \
     --data-binary "{\"app_id\" : \"APP_ID\",
    \"tags\":{\"a\":\"\",\"foo\":\"\"}}" \
     https://onesignal.com/api/v1/players/PLAYER_ID

TIP

To delete a tag, include its key and set its value to blank. Omitting a key/value will not delete it.

This requires all the devices and tags. You can use the API, or export from the Audience view to get a CSV file with all the devices.

I used this PHP script to parse the CSV, delete tags I didn’t want, and keep those I wanted to keep. Note: edit the CSV and keep only the player_id and tags columns.

<?php

$app_id = "xxxxxxxxxxxxxxxxxxxxxxxx";

$onesig = null;

if (($handle = fopen("onesignal_users.csv", "r")) !== FALSE) {
    while (($data = fgetcsv($handle, 0, ",")) !== FALSE) {
        $num = count($data);

        // no tags = "{}", so skip them
        if (strlen($data[1])>2) {
            $onesig[$data[0]] = json_decode($data[1], true);
        }
    }

    fclose($handle);
}

// print_r($onesig);

$fields = array( 
    'app_id' => $app_id , 
    'tags' => array(
             'locale' => '',
            'device' => '',
            'apnsEnv' => '',
	        'isChinese' => '',
	        'appVersion' => '',
	        'systemLang' => '',
	        'kPushEnabled' => '',
	        'systemVersion' => '',
	        'kMigratedToOneSignalWithDefaultPlist' => '',
	        'kMigratedToOneSignal' => '',
	        'isRemoveAdsPurchased' => '',
	        'oneSignalSDKVersion' => '',
	        'deviceToken' => '',
	        'is_test' => '',
	        '1' => '',
	        '2' => '',
	        '3' => '',
	        '4' => '',
	        '5' => '',
	        '6' => '',
	        '7' => '',
	        '8' => '',
	        '9' => '',
	        '10' => '',
	        '11' => '',
	        '12' => '',
	        '13' => '',
	        '14' => '',
	        '15' => '',
	        '16' => '',
	        '17' => '',
	        '18' => '',
	        '19' => '',
	        '20' => '',
	        '21' => ''
        )
    );

// take a copy to mut
$mutFields = $fields;
$maxNO = 0;
$cc = 0;

foreach ($array_keys as $array_key) {
    $count = 0;
    $cc++;
    $mutFields = $fields; // new id so reset to default fields

    // if a tag 1-22 exists and is set to NO, keep it by updating mutFields
    for($i=1; $i < 22; $i++){
        if(isset($onesig[$array_key][$i])){
            if($onesig[$array_key][$i] == "NO"){
                $mutFields['tags']["$i"] = 'NO';
                $count++;
            }
        }
    }

    // just an FYI for me
    if($count > $maxNO){
        $maxNO = $count;
    }

    $mutFields = json_encode($mutFields);

	// send to OneSignal
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, 'https://onesignal.com/api/v1/players/'.$array_key);
    curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HEADER, false);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
    curl_setopt($ch, CURLOPT_POSTFIELDS, $mutFields);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    $response = curl_exec($ch);
    curl_close($ch);
    
    $resultData = json_decode($response, true); // could check for errors...
    echo "id: $array_key\n";
    echo "count: $cc \n";
    // print_r($resultData);  
}

echo "max: $maxNO \n";

For 35,000+ devices, it took around 4 hours, but now my devices have less than 10 tags.

On the iOS side, I did two things:

Clear out old tags on startup

- (void)cleanUpOldTags {

    BOOL clean = NO;

    if([_defaults.dictionaryRepresentation.allKeys containsObject:kReCleanedAllTags]){
        clean = [_defaults boolForKey:kReCleanedAllTags];
    }

    if(clean == NO) {

        NSArray *tagsToClean =  @[@"ParseObjectId",
                                  @"installationId",
                                  @"device",
                                  @"apnsEnv",
                                  @"is_test",
                                  @"isChinese",
                                  @"appVersion",
                                  @"kPushEnabled",
                                  @"systemVersion",
                                  @"oneSignalSDKVersion",
                                  @"kMigratedToOneSignal",
                                  @"KREMIGRATEDTOONESIGNAL",
                                  @"isRemoveAdsPurchased",
                                  @"locale",
                                  @"UDID",
                                  @"deviceToken",
                                  @"systemLang",
                                  @"kMigratedToOneSignalWithDefaultPlist"
                                  ];

        [OneSignal deleteTags:tagsToClean onSuccess:^(NSDictionary *results2) {
            CLS_LOG(@"All tags deleted.");
            [self->_defaults setBool:YES forKey:kReCleanedAllTags];
        } onFailure:^(NSError *error) {
            CLS_LOG(@"Error deleting all tags: %@", error);
        }];
    }
    else {
        CLS_LOG(@"Already cleaned up all tags");
    }
}

Ensure users can only opt-out of a maximum of 10 warnings

 if(optOutCount >= kMaxNumOfOptOuts){

    // show alert
    [Utilities showAlert: NSLocalizedString( @"Sorry", @"Sorry")
                 message: NSLocalizedString( @"You can only deselect a maximum of 10 warnings.", @"You can deselct a maximum of 10 warnings")];
}

  1. There are 21 possible warnings issued by the HKO, people can opt out of certain warnings. If they do, I set a tag on the device in OneSignal.