Limit copying Google Analytics 4 Metrics from another property (API V1) - javascript

I use the following script on Google AppScript (derived from a constribution found here : Copy Google Analytics custom definitions/dimensions between GA4 properties with the Google Analytics Data API?
to copy more than 115 custom metrics (account is GA4 360) from one property to 35 other (local countries) ones.
Problem is, it seems that the script (related to the API v1 beta) only grabs the first 50 results and not the 115 that I have on my property.
It seems normal and the API documentation mentions a parameter to use that would be 'pageSize' but I can't manage to use this parameter on my script.
See : https://developers.google.com/analytics/devguides/config/admin/v1/rest/v1alpha/properties.customMetrics/list?hl=en
"pageSize => integer
The maximum number of resources to return. If unspecified, at most 50 resources will be returned. The maximum value is 200 (higher values will be coerced to the maximum)."
Do you have any clue how to modify my script (see below) in order to set pageSize limit to 150 custom metrics ?
Thanks for your help.
function main(){
gaSourcePropertyId = 'XXXXX'; // Property to copy from
gaDestinationPropertyId = 'XXX'; // Property to paste to
copyCustomMetrics(gaSourcePropertyId, gaDestinationPropertyId);
}
function copyCustomMetrics(gaSourcePropertyId, gaDestinationPropertyId) {
let sourceMetrics = getCustomMetrics(gaSourcePropertyId);
addCustomMetrics(gaDestinationPropertyId, sourceMetrics);
}
function getCustomMetrics(gaPropertyId) {
try {
return AnalyticsAdmin.Properties.CustomMetrics.list(`properties/${gaPropertyId}`)['customMetrics'];
} catch (error) {
console.error(error);
}
}
function addCustomMetrics(gaPropertyId, metrics) {
let destinationCustomMetrics = getCustomMetrics(gaPropertyId);
// The destination may not have any custom metrics.
if (typeof destinationCustomMetrics == 'undefined') {
console.info(`Could not get custom metrics for property ID '${gaPropertyId}'.`, destinationCustomMetrics);
destinationCustomMetrics = [];
};
const destinationMetricsParameterNames = destinationCustomMetrics.map(metric=>metric['parameterName']);
metrics.forEach((sourceMetric)=>{
// Check if the new metric already exists in the destination.
if ( !destinationMetricsParameterNames.includes(sourceMetric['parameterName']) ) {
let destinationMetric = {
"parameterName": sourceMetric['parameterName'],
"displayName": sourceMetric['displayName'],
"description": sourceMetric['description'],
"scope": sourceMetric['scope'],
"measurementUnit": "STANDARD" // To be changed regarding the type of unit of measurment if not standard on property source
};
let result = null;
try {
result = AnalyticsAdmin.Properties.CustomMetrics.create(destinationMetric, `properties/${gaPropertyId}`);
console.log('[COPIED]',result);
} catch (error) {
console.log('[FAILED]', destinationMetric)
console.error(error);
}
} else {
console.info(`[NO ACTION] Metric with apiName '${ sourceMetric['parameterName'] }' already present in destination! Metric not added to destination.`, sourceMetric);
}
});
}

Related

Script runs smoothly from start to finish, but the expected result doesn't happen

The project aims to study a new social media:
https://booyah.live/
My needs are:
1 - Collect data from profiles that follow a specific profile.
2 - My account use this data to follow the collected profiles.
3 - Among other possible options, also unfollow the profiles I follow.
The problem found in the current script:
The profile data in theory is being collected, the script runs perfectly until the end, but for some reason I can't specify, instead of following all the collected profiles, it only follows the base profile.
For example:
I want to follow all 250 profiles that follow the ID 123456
I activate the booyahGetAccounts(123456); script
In theory the end result would be my account following 250 profiles
But the end result I end up following only the 123456 profile, so the count of people I'm following is 1
Complete Project Script:
const csrf = 'MY_CSRF_TOKEN';
async function booyahGetAccounts(uid, type = 'followers', follow = 1) {
if (typeof uid !== 'undefined' && !isNaN(uid)) {
const loggedInUserID = window.localStorage?.loggedUID;
if (uid === 0) uid = loggedInUserID;
const unfollow = follow === -1;
if (unfollow) follow = 1;
if (loggedInUserID) {
if (csrf) {
async function getUserData(uid) {
const response = await fetch(`https://booyah.live/api/v3/users/${uid}`),
data = await response.json();
return data.user;
}
const loggedInUserData = await getUserData(loggedInUserID),
targetUserData = await getUserData(uid),
followUser = uid => fetch(`https://booyah.live/api/v3/users/${loggedInUserID}/followings`, { method: (unfollow ? 'DELETE' : 'POST'), headers: { 'X-CSRF-Token': csrf }, body: JSON.stringify({ followee_uid: uid, source: 43 }) }),
logSep = (data = '', usePad = 0) => typeof data === 'string' && usePad ? console.log((data ? data + ' ' : '').padEnd(50, '━')) : console.log('━'.repeat(50),data,'━'.repeat(50));
async function getList(uid, type, follow) {
const isLoggedInUser = uid === loggedInUserID;
if (isLoggedInUser && follow && !unfollow && type === 'followings') {
follow = 0;
console.warn('You alredy follow your followings. `follow` mode switched to `false`. Followings will be retrieved instead of followed.');
}
const userData = await getUserData(uid),
totalCount = userData[type.slice(0,-1)+'_count'] || 0,
totalCountStrLength = totalCount.toString().length;
if (totalCount) {
let userIDsLength = 0;
const userIDs = [],
nickname = userData.nickname,
nicknameStr = `${nickname ? ` of ${nickname}'s ${type}` : ''}`,
alreadyFollowedStr = uid => `User ID ${uid} already followed by ${loggedInUserData.nickname} (Account #${loggedInUserID})`;
async function followerFetch(cursor = 0) {
const fetched = [];
await fetch(`https://booyah.live/api/v3/users/${uid}/${type}?cursor=${cursor}&count=100`).then(res => res.json()).then(data => {
const list = data[type.slice(0,-1)+'_list'];
if (list?.length) fetched.push(...list.map(e => e.uid));
if (fetched.length) {
userIDs.push(...fetched);
userIDsLength += fetched.length;
if (follow) followUser(uid);
console.log(`${userIDsLength.toString().padStart(totalCountStrLength)} (${(userIDsLength / totalCount * 100).toFixed(4)}%)${nicknameStr} ${follow ? 'followed' : 'retrieved'}`);
if (fetched.length === 100) {
followerFetch(data.cursor);
} else {
console.log(`END REACHED. ${userIDsLength} accounts ${follow ? 'followed' : 'retrieved'}.`);
if (!follow) logSep(targetList);
}
}
});
}
await followerFetch();
return userIDs;
} else {
console.log(`This account has no ${type}.`);
}
}
logSep(`${follow ? 'Following' : 'Retrieving'} ${targetUserData.nickname}'s ${type}`, 1);
const targetList = await getList(uid, type, follow);
} else {
console.error('Missing CSRF token. Retrieve your CSRF token from the Network tab in your inspector by clicking into the Network tab item named "bug-report-claims" and then scrolling down in the associated details window to where you see "x-csrf-token". Copy its value and store it into a variable named "csrf" which this function will reference when you execute it.');
}
} else {
console.error('You do not appear to be logged in. Please log in and try again.');
}
} else {
console.error('UID not passed. Pass the UID of the profile you are targeting to this function.');
}
}
This current question is a continuation of that answer from the link:
Collect the full list of buttons to follow without having to scroll the page (DevTools Google Chrome)
Since I can't offer more bounty on that question, I created this one to offer the new bounty to anyone who can fix the bug and make the script work.
Access account on Booyah website to use for tests:
Access by google:
User: teststackoverflowbooyah#gmail.com
Password: quartodemilha
I have to admit that it is really hard to read your code, I spent a lesser amount of time rewriting everything from scratch.
Stated that we need a code piece to be cut/pasted in the JavaScript console of web browsers able to store some data (i.e. expiration of followings and permanent followings) we need some considerations.
We can consider expiration of followings as volatile data: something that if lost can be reset to 1 day later from when we loose this data. window.localStorage is a perfect candidate to store these kind of data. If we change web browser the only drawback is that we loose the expiration of followings and we can tolerate to reset them to 1 day later from when we change browser.
While to store the list of permanent followings we need a permanent store even if we change web browser. The best idea that came to my mind is to create an alternative account with which to follow the users we never want to stop following. In my code I used uid 3186068 (a random user), once you have created your own alternative account, just replace the first line of the code block with its uid.
Another thing we need to take care is error handling: API could always have errors. The approach I chosen is to write myFetch which, in case of errors, retries twice the same call; if the error persists, probably we are facing a temporary booyah.live outage. Probably we just need to retry a bit later.
To try to provide a comfortable interface, the code blocks gathers the uid from window.location: to follow the followers of users, just cut/paste the code block on tabs opened on their profiles. For example I run the code from a tab open on https://booyah.live/studio/123456?source=44.
Last, to unfollow users the clean function is called 5 minutes later we paste the code (to not conflict with calls to follow followers) and than is executed one hour later it finishes its job. It is written to access the localStorage in an atomic way, so you can have many of them running simultaneously on different tabs of the same browser, you can not care about it. The only thing you need to take care it that when the window.location changes, all the JavaScript events in the tab are reset; so I suggest to keep a tab open on the home page, paste the code block on it, and forget about this tab; it will be the tab responsible of unfollowing users. Then open other tabs to do what you need, when you hit a user you want to follow the followers, paste the block on it, wait the job is finished and continue to use the tab normally.
// The account we use to store followings
const followingsUID = 3186068;
// Gather the loggedUID from window.localStorage
const { loggedUID } = window.localStorage;
// Gather the CSRF-Token from the cookies
const csrf = document.cookie.split("; ").reduce((ret, _) => (_.startsWith("session_key=") ? _.substr(12) : ret), null);
// APIs could have errors, let's do some retries
async function myFetch(url, options, attempt = 0) {
try {
const res = await fetch("https://booyah.live/api/v3/" + url, options);
const ret = await res.json();
return ret;
} catch(e) {
// After too many consecutive errors, let's abort: we need to retry later
if(attempt === 3) throw e;
return myFetch(url, option, attempt + 1);
}
}
function expire(uid, add = true) {
const { followingsExpire } = window.localStorage;
let expires = {};
try {
// Get and parse followingsExpire from localStorage
expires = JSON.parse(followingsExpire);
} catch(e) {
// In case of error (ex. new browsers) simply init to empty
window.localStorage.followingsExpire = "{}";
}
if(! uid) return expires;
// Set expire after 1 day
if(add) expires[uid] = new Date().getTime() + 3600 * 24 * 1000;
else delete expires[uid];
window.localStorage.followingsExpire = JSON.stringify(expires);
}
async function clean() {
try {
const expires = expire();
const now = new Date().getTime();
for(const uid in expires) {
if(expires[uid] < now) {
await followUser(parseInt(uid), false);
expire(uid, false);
}
}
} catch(e) {}
// Repeat clean in an hour
window.setTimeout(clean, 3600 * 1000);
}
async function fetchFollow(uid, type = "followers", from = 0) {
const { cursor, follower_list, following_list } = await myFetch(`users/${uid}/${type}?cursor=${from}&count=50`);
const got = (type === "followers" ? follower_list : following_list).map(_ => _.uid);
const others = cursor ? await fetchFollow(uid, type, cursor) : [];
return [...got, ...others];
}
async function followUser(uid, follow = true) {
console.log(`${follow ? "F" : "Unf"}ollowing ${uid}...`);
return myFetch(`users/${loggedUID}/followings`, {
method: follow ? "POST" : "DELETE",
headers: { "X-CSRF-Token": csrf },
body: JSON.stringify({ followee_uid: uid, source: 43 })
});
}
async function doAll() {
if(! loggedUID) throw new Error("Can't get 'loggedUID' from localStorage: try to login again");
if(! csrf) throw new Error("Can't get session token from cookies: try to login again");
console.log("Fetching current followings...");
const currentFollowings = await fetchFollow(loggedUID, "followings");
console.log("Fetching permanent followings...");
const permanentFollowings = await fetchFollow(followingsUID, "followings");
console.log("Syncing permanent followings...");
for(const uid of permanentFollowings) {
expire(uid, false);
if(currentFollowings.indexOf(uid) === -1) {
await followUser(uid);
currentFollowings.push(uid);
}
}
// Sync followingsExpire in localStorage
for(const uid of currentFollowings) if(permanentFollowings.indexOf(uid) === -1) expire(uid);
// Call first clean task in 5 minutes
window.setTimeout(clean, 300 * 1000);
// Gather uid from window.location
const match = /\/studio\/(\d+)/.exec(window.location.pathname);
if(match) {
console.log("Fetching this user followers...");
const followings = await fetchFollow(parseInt(match[1]));
for(const uid of followings) {
if(currentFollowings.indexOf(uid) === -1) {
await followUser(uid);
expire(uid);
}
}
}
return "Done";
}
await doAll();
The problem: I strongly suspect a booyah.live API bug
To test my code I run it from https://booyah.live/studio/123456?source=44.
If I run it multiple times I continue to get following output:
Fetching current followings...
Fetching permanent followings...
Syncing permanent followings...
Following 1801775...
Following 143823...
Following 137017...
Fetching this user followers...
Following 16884042...
Following 16166724...
There is bug somewhere! The expected output for subsequent executions in the same tab would be:
Fetching current followings...
Fetching permanent followings...
Syncing permanent followings...
Fetching this user followers...
After seeking the bug in my code without success, I checked booyah.live APIs: if I navigate following URLs (the uids are the ones the code continue to follow in subsequent executions)
https://booyah.live/studio/1801775
https://booyah.live/studio/143823
https://booyah.live/studio/137017
https://booyah.live/studio/16884042
https://booyah.live/studio/16166724
I can clearly see I follow them, but if I navigate https://booyah.live/following (the list of users I follow) I can't find them, neither if I scroll the page till the end.
Since I do exactly the same calls the website does, I strongly suspect the bug is in booyah.live APIs, exactly in the way they handle the cursor parameter.
I suggest you to open a support ticket to booyah.live support team. You could use the test account you provided us: I already provided you the details to do that. ;)

Not including 'gas' or 'gasPrice' attributes in the 'options' object inside of a send() call

Does anyone know what the send() function call to a smart contract method defaults to when you don't specify gas or gasPrice? Does it automatically allocate sufficient gas and calculate the current average gasPrice? And are those attributes always optional or are there situations where including either one is mandatory?
From the documentation, both gas and gas seem to always be optional.
Unfortunately the documentation doesn't state what those will default to when not provided, but having a quick peak at the code (hopefully that's the right code path) it seems that it calls getGasPrice internally to get the gas price and then default the gasPrice to that.
// Send the actual transaction
if (isSendTx && _.isObject(payload.params[0]) && typeof payload.params[0].gasPrice === 'undefined') {
var getGasPrice = (new Method({
name: 'getGasPrice',
call: 'eth_gasPrice',
params: 0
})).createFunction(method.requestManager);
getGasPrice(function (err, gasPrice) {
if (gasPrice) {
payload.params[0].gasPrice = gasPrice;
}
if (isSendTx) {
setTimeout(() => {
defer.eventEmitter.emit('sending', payload);
}, 0);
}
sendRequest(payload, method);
});
GitHub Source

How to increase browser localstorage size [duplicate]

I've written a webapp that allows you to store the images in the localStorage until you hit save (so it works offline, if signal is poor).
When the localStorage reaches 5MB Google Chrome produces an error in the javascript console log:
Uncaught Error: QUOTA_EXCEEDED_ERR: DOM Exception 22
How do I increase the size of the localStorage quota on Google Chrome?
5MB is a hard limit and that is stupid. IndexedDB gives you ~50MB which is more reasonable. To make it easier to use try Dexie.js https://github.com/dfahlander/Dexie.js
Update:
Dexie.js was actually still an overkill for my simple key-value purposes so I wrote this much simpler script https://github.com/DVLP/localStorageDB
with this you have 50MB and can get and set values like that
// Setting values
ldb.set('nameGoesHere', 'value goes here');
// Getting values - callback is required because the data is being retrieved asynchronously:
ldb.get('nameGoesHere', function (value) {
console.log('And the value is', value);
});
Copy/paste the line below so ldb.set() and ldb.get() from the example above will become available.
!function(){function e(t,o){return n?void(n.transaction("s").objectStore("s").get(t).onsuccess=function(e){var t=e.target.result&&e.target.result.v||null;o(t)}):void setTimeout(function(){e(t,o)},100)}var t=window.indexedDB||window.mozIndexedDB||window.webkitIndexedDB||window.msIndexedDB;if(!t)return void console.error("indexDB not supported");var n,o={k:"",v:""},r=t.open("d2",1);r.onsuccess=function(e){n=this.result},r.onerror=function(e){console.error("indexedDB request error"),console.log(e)},r.onupgradeneeded=function(e){n=null;var t=e.target.result.createObjectStore("s",{keyPath:"k"});t.transaction.oncomplete=function(e){n=e.target.db}},window.ldb={get:e,set:function(e,t){o.k=e,o.v=t,n.transaction("s","readwrite").objectStore("s").put(o)}}}();
You can't, it's hard-wired at 5MB. This is a design decision by the Chrome developers.
In Chrome, the Web SQL db and cache manifest also have low limits by default, but if you package the app for the Chrome App Store you can increase them.
See also Managing HTML5 Offline Storage - Google Chrome.
The quota is for the user to set, how much space he wishes to allow to each website.
Therefore since the purpose is to restrict the web pages, the web pages cannot change the restriction.
If storage is low, you can prompt the user to increase local storage.
To find out if storage is low, you could probe the local storage size by saving an object then deleting it.
You can't but if you save JSON in your localStorage you can use a library to compress data like : https://github.com/k-yak/JJLC
demo : http://k-yak.github.io/JJLC/
Here you can test your program , you should handle also the cases when the cuota is exceed
https://stackoverflow.com/a/5664344/2630686 The above answer is much amazing. I applied it in my project and implement a full solution to request all kinds of resource.
// Firstly reference the above ldb code in the answer I mentioned.
export function get_file({ url, d3, name, enable_request = false }) {
if (name === undefined) { // set saved data name by url parsing alternatively
name = url.split('?')[0].split('/').at(-1).split('.')[0];
}
const html_name = location.href.split('/').at(-1).split('.')[0]
name = `${html_name}_${name}`
let ret = null;
const is_outer = is_outer_net(url); // check outer net url by its start with http or //
// try to access data from local. Return null if not found
if (is_outer && !enable_request) {
if (localStorage[name]) {
ret = new Promise(resolve => resolve(JSON.parse(localStorage[name])));
} else {
ret = new Promise(r => {
ldb.get(name, function (value) {
r(value)
})
});
}
} else {
ret = new Promise(r => r(null))
}
ret.then(data => {
if (data) {
return data
} else {
const method = url.split('.').at(-1)
// d3 method supported
if (d3 && d3[method]) {
ret = d3[method](url)
} else {
if (url.startsWith('~/')) { // local files accessed supported. You need a local service that can return local file data by requested url's address value
url = `http://localhost:8010/get_file?address=${url}`
}
ret = fetch(url).then(data => {
// parse data by requested data type
if (url.endsWith('txt')) {
return data.text()
} else {
return data.json()
}
})
}
ret = ret.then(da => {
data = da
if (is_outer) { // save data to localStorage firstly
localStorage[name] = JSON.stringify(data);
}
}).catch(e => { // save to ldb if 5MB exceed
ldb.set(name, data);
}).finally(_ => {
return data;
});
}
})
return ret;
}

Google Drive REST API v2 file properties array in Google Apps Script is undefined?

I am working to manage some Google Drive files with Google Apps Script. One piece of this project is reviewing properties on files, so I am using the Drive API rather than DriveApp. Additionally, Google Apps Script currently has access to the Drive REST API v2 instead of v3.
I've successfully set a property (id) and am able to pull the files with the property set.
console = Logger;
function Files (folderId) {
var optionalArgs,response
;
optionalArgs = {
q:'"'+folderId+'" in parents',
spaces:"drive"
}
do {
response = Drive.Files.list(optionalArgs);
response.items.forEach(function (file) {
console.log(file.properties);
var id = file.properties.find(function (property) {
return property.key === 'id';
});
this[id.value] = file;
},this);
} while (optionalArgs.pageToken = response.nextPageToken);
}
When running this function, I am able to see the file properties in the log
[{visibility=PUBLIC, kind=drive#property, etag="3GjDSTzy841RsmcBo4Ir-DLlp20/HGzJl78t8I2IehiAlaGXTkm2-C4", value=9e18766b-1cc9-4c1b-8003-b241f43db304, key=id}]
but get
TypeError :Cannot call method "find" of undefined.
I am unable to iterate through this resulting array. Using JSON.parse on it trys to convert it to an object, which is problematic for files with multiple properties. Using JSON.parse(JSON.stringify()) on it results in
SyntaxError: Unexpected token: u
which I understand is resulting from the value being undefined. I could work with that, if my log wasn't telling me otherwise.
"nextPageToken(string) The page token for the next page of files. This will be absent if the end of the files list has been reached."
Quote from: https://developers.google.com/drive/v2/reference/files/list#examples
so this line assigns an undefined value to optionalArgs.pageToken
while (optionalArgs.pageToken = response.nextPageToken);
and does not terminate the loop. A better way is to assign the value of optionalArgs.pageToken inside the loop and break the loop if its value is undefined like this:
console = Logger;
function Files (folderId) {
var optionalArgs,response
;
optionalArgs = {
q:'"'+folderId+'" in parents',
spaces:"drive"
}
do {
response = Drive.Files.list(optionalArgs);
response.items.forEach(function (file) {
console.log(file.properties);
var id = file.properties.find(function (property) {
return property.key === 'id';
});
this[id.value] = file;
},this);
// Assign the value inside the loop
optionalArgs.pageToken = response.nextPageToken
} while (optionalArgs.pageToken != undefined) // check to see if it undefined here and break the loop.
}
You can use v3 in Google Apps Script by selecting the version in the Advance Google Service option:
Just note that you have to follow how to request in Google Drive v3 but you can still use v2 because all of the example is still in v2.
Regarding your error, using their code in the sample code for Listing folders.
function listRootFolders() {
var query = '"root" in parents and trashed = false and ' +
'mimeType = "application/vnd.google-apps.folder"';
var folders, pageToken;
do {
folders = Drive.Files.list({
q: query,
maxResults: 100,
pageToken: pageToken
});
if (folders.items && folders.items.length > 0) {
for (var i = 0; i < folders.items.length; i++) {
var folder = folders.items[i];
Logger.log('%s (ID: %s)', folder.title, folder.id);
}
} else {
Logger.log('No folders found.');
}
pageToken = folders.nextPageToken;
} while (pageToken);
}
You can also read more code implementation of the above documentation.
Hope this helps.

How do I get information about the type of connection of a WebRTC PeerConnection?

Is there any way to get information about the type of connection used in WebRTC in a programmatic way?
For example in my app I use local connections as well as STUN and TURN. From the ICE candidates I can gather if the type of the candidates is host or relay, and on the server I can see if the connection is attempted via STUN (connection initiation) or TURN (steady stream during connection).
Up until now I could not find a way to access the information about the finally used type of connection in the browser. There are the candidates, the browser stops gathering and then there is a working connection. Looking through the events I couldn't find any information though.
I know that Chrome supports getStats() on the peerconnection, which allows me access to most of the information found in chrome://webrtc-internals, however I didn't find this information there either.
Is there any way to access this information from javascript?
Thank you very much.
According to the specification, which is currently implemented in Firefox, but not in Chrome, you can indeed suss out the active candidate from the statistics available for candidate pairs, which are:
dictionary RTCIceCandidatePairStats : RTCStats {
DOMString transportId;
DOMString localCandidateId;
DOMString remoteCandidateId;
RTCStatsIceCandidatePairState state;
unsigned long long priority;
boolean nominated;
boolean writable;
boolean readable;
unsigned long long bytesSent;
unsigned long long bytesReceived;
double roundTripTime;
double availableOutgoingBitrate;
double availableIncomingBitrate;
};
Combined with the stats on the individual candidates:
dictionary RTCIceCandidateAttributes : RTCStats {
DOMString ipAddress;
long portNumber;
DOMString transport;
RTCStatsIceCandidateType candidateType;
long priority;
DOMString addressSourceUrl;
};
Use peerConnection.getStats() to look for an ice candidate pair that is both nominated and has succeeded:
pc.getStats(null))
.then(function(stats) {
return Object.keys(stats).forEach(function(key) {
if (stats[key].type == "candidatepair" &&
stats[key].nominated && stats[key].state == "succeeded") {
var remote = stats[stats[key].remoteCandidateId];
console.log("Connected to: " + remote.ipAddress +":"+
remote.portNumber +" "+ remote.transport +
" "+ remote.candidateType);
}
});
})
.catch(function(e) { console.log(e.name); });
This might output something like:
Connected to: 192.168.1.2:49190 udp host
which you could test against the LAN range. If instead it returned something like:
Connected to: 24.57.143.7:61102 udp relayed
then you'd have a TURN connection.
Here's a jsfiddle that shows this (requires Firefox Developer Edition for other reasons).
It took me a long time to get this right, so hopefully this helps someone.
The new way
You can now get the selected candidate pair from the RTCPeerConnection without the stats api:
const pair = rtcConnection.sctp.transport.iceTransport.getSelectedCandidatePair();
console.log(pair.remote.type);
At the time of writing (October 2, 2020) this only works in Chromium however.
You can still use the stats api for other browsers.
Also note the comment below by jib that this only works if you have a DataChannel.
For browsers without getSelectedCandidatePair() support
According to https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidatePairStats
(at the bottom of the page at the selected property.
The spec-compliant way to determine the selected candidate pair is to look for a stats object of type transport, which is an RTCTransportStats object. That object's selectedCandidatePairId property indicates whether or not the specified transport is the one being used.
So trying to find the selected pair using stat.nominated && stats.state == "succeeded" is not the right way to do it.
Instead, you can get it by looking at the selected pair in the transport stat. Firefox doesn't support this, but fortunately there is a non-standard selected property in candidate pairs for firefox.
const stats = await rtcConnection.getStats();
if(stats){
let selectedPairId = null;
for(const [key, stat] of stats){
if(stat.type == "transport"){
selectedPairId = stat.selectedCandidatePairId;
break;
}
}
let candidatePair = stats.get(selectedPairId);
if(!candidatePair){
for(const [key, stat] of stats){
if(stat.type == "candidate-pair" && stat.selected){
candidatePair = stat;
break;
}
}
}
if(candidatePair){
for(const [key, stat] of stats){
if(key == candidatePair.remoteCandidateId){
return stat.candidateType;
}
}
}
}
jib's answer from March 2015 is very helpful, but doesn't work with Firefox v65 nor Chrome v72 (on Windows) in March 2019. Two updates are needed:
1) The "stats" value has type RTCStatsReport in both browsers now, and it's an iterable object with no keys. So, iterate it with forEach(report => {...}) and "report" will be an object with keys like those that jib shows for "stats".
2) "candidatepair" isn't a valid value of report.type but "candidate-pair" is.
Thanks to #DavidP and a more in-depth answer I wrote the code below to get the ICE Candidates type.
Updated Code:
Getting the ICE candidates with conncectionStats
function getCandidateIds(stats) {
let ids = {}
stats.forEach(report => {
if (report.type == "candidate-pair" && report.nominated && report.state == "succeeded") {
//console.log("Found IDs")
ids = {
localId: report.localCandidateId,
remoteId: report.remoteCandidateId
}
}
});
return ids
}
function getCandidateInfo(stats, candidateId) {
let info = null
stats.forEach(report => {
if (report.id == candidateId) {
console.log("Found Candidate")
info = report
}
})
return info
}
async function conncectionStats() {
const stats = await this.pc.getStats(null)
const candidates = await this.getCandidateIds(stats)
console.log("candidates: ", candidates)
if (candidates !== {}) {
const localCadidate = await this.getCandidateInfo(stats, candidates.localId)
const remoteCadidate = await this.getCandidateInfo(stats, candidates.remoteId)
if (localCadidate !== null && remoteCadidate !== null) {
return [localCadidate, remoteCadidate]
}
}
// we did not find the candidates for whatever reeason
return [null, null]
}
reading out IP:
let myAddress = ""
let peerAddress = ""
if (localCadidate.hasOwnProperty("ip")){
myAddress = localCadidate.ip
peerAddress = remoteCadidate.ip
} else {
myAddress = localCadidate.address
peerAddress = remoteCadidate.address
}
old version:
function getConnectionDetails(pc){
pc.getStats(null)
.then(function(stats) {
stats.forEach(report => {
if (report.type == "candidate-pair"
&& report.nominated
&& report.state == "succeeded")
{
console.log( "Local ICE:", report.localCandidateId)
console.log( "Remote ICE:",report.remoteCandidateId)
getCandidates(pc, report.localCandidateId, report.remoteCandidateId)
}
});
})
.catch(function(e) { console.log(e.name); });
};
function getCandidates(pc, localId, remoteId){
//console.log("looking for candidates")
pc.getStats(null)
.then(function(stats) {
stats.forEach(report => {
if (report.id == localId) {
console.log("Local: Type:", report.candidateType," IP:", report.ip)
} else if (report.id == remoteId){
console.log("Remote: Type:", report.candidateType," IP:", report.ip)
}
})
})
.catch(function(e) { console.log(e.name); });
}
You may not need both candidates depending on what information should be extracted.

Categories